diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c3ef9fa088..b89f6ccce0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,17 +10,25 @@ on: - "!*-fdroid" - "!*-armv7a" pull_request: - paths-ignore: - - "apps/ios" - - "apps/multiplatform" - - "blog" - - "docs" - - "fastlane" - - "images" - - "packages" - - "website" - - "README.md" - - "PRIVACY.md" + paths: + - "src/**" + - "apps/simplex-chat/**" + - "apps/simplex-bot/**" + - "apps/simplex-bot-advanced/**" + - "apps/simplex-broadcast-bot/**" + - "apps/simplex-directory-service/**" + - "tests/**" + - "bots/src/**" + - "simplex-chat.cabal" + - "cabal.project" + - "Dockerfile*" + - "scripts/ci/**" + - "scripts/desktop/**" + - ".github/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/v') }} # This workflow uses custom actions (prepare-build and prepare-release) defined in: # @@ -294,6 +302,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true shell: docker exec -t builder sh -eu {0} run: | + export ASSETS_DIR='../../assets' scripts/desktop/make-deb-linux.sh - name: Prepare Desktop @@ -319,6 +328,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') && matrix.os == '22.04' && matrix.should_run == true shell: docker exec -t builder sh -eu {0} run: | + export ASSETS_DIR='../../assets' scripts/desktop/make-appimage-linux.sh - name: Prepare AppImage @@ -369,6 +379,100 @@ jobs: exit 1 fi +# ================================= +# Linux PostgreSQL Library Build +# ================================= + + build-linux-postgres: + name: "ubuntu-22.04-x86_64 (Postgres lib), GHC: ${{ needs.variables.outputs.GHC_VER }}" + needs: [maybe-release, variables] + runs-on: ubuntu-22.04 + if: startsWith(github.ref, 'refs/tags/v') + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Get UID and GID + id: ids + run: | + echo "uid=$(id -u)" >> $GITHUB_OUTPUT + echo "gid=$(id -g)" >> $GITHUB_OUTPUT + + - name: Free disk space + shell: bash + run: ./scripts/ci/linux_util_free_space.sh + + - name: Restore cached build + uses: actions/cache@v4 + with: + path: | + ~/.cabal/store + dist-newstyle + key: ubuntu-22.04-x86_64-postgres-ghc${{ needs.variables.outputs.GHC_VER }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }} + + - name: Set up Docker Buildx + uses: simplex-chat/docker-setup-buildx-action@v3 + + - name: Build and cache Docker image + uses: simplex-chat/docker-build-push-action@v6 + with: + context: . + load: true + file: Dockerfile.build + tags: build/22.04:latest + build-args: | + TAG=22.04 + HASH=sha256:5c8b2c0a6c745bc177669abfaa716b4bc57d58e2ea3882fb5da67f4d59e3dda5 + GHC=${{ needs.variables.outputs.GHC_VER }} + USER_UID=${{ steps.ids.outputs.uid }} + USER_GID=${{ steps.ids.outputs.gid }} + + - name: Start container + shell: bash + run: | + docker run -t -d \ + --name builder \ + -v ~/.cabal:/root/.cabal \ + -v /home/runner/work/_temp:/home/runner/work/_temp \ + -v ${{ github.workspace }}:/project \ + build/22.04:latest + + - name: Prepare cabal.project.local + shell: bash + run: | + echo "ignore-project: False" >> cabal.project.local + echo "package direct-sqlcipher" >> cabal.project.local + echo " flags: +openssl" >> cabal.project.local + + - name: Build postgres library + shell: docker exec -t builder sh -eu {0} + run: | + cabal clean + cabal update + scripts/desktop/build-lib-linux.sh postgres + + - name: Copy libs from container + shell: bash + run: | + ARCH=x86_64 + GHC_VER=${{ needs.variables.outputs.GHC_VER }} + BUILD_DIR=$(echo dist-newstyle/build/${ARCH}-linux/ghc-${GHC_VER}/simplex-chat-*) + mkdir -p postgres-libs + cp ${BUILD_DIR}/build/libsimplex.so postgres-libs/ + cp ${BUILD_DIR}/build/deps/* postgres-libs/ + + - name: Upload postgres libs artifact + uses: actions/upload-artifact@v4 + with: + name: simplex-libs-linux-postgres-x86_64 + path: postgres-libs/ + + - name: Fix permissions for cache + shell: bash + run: | + sudo chmod -R 777 dist-newstyle ~/.cabal + sudo chown -R $(id -u):$(id -g) dist-newstyle ~/.cabal + # ========================= # MacOS Build # ========================= @@ -447,6 +551,7 @@ jobs: APPLE_SIMPLEX_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_APPLE_ID }} APPLE_SIMPLEX_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_PASSWORD }} run: | + export ASSETS_DIR='../../assets' scripts/ci/build-desktop-mac.sh path=$(echo $PWD/apps/multiplatform/release/main/dmg/SimpleX-*.dmg) echo "package_path=$path" >> $GITHUB_OUTPUT @@ -573,7 +678,7 @@ jobs: export PATH=$PATH:/c/ghcup/bin:$(echo /c/tools/ghc-*/bin || echo) scripts/desktop/build-lib-windows.sh cd apps/multiplatform - ./gradlew packageMsi + ./gradlew -Psimplex.assets.dir=../../assets packageMsi rm -rf dist-newstyle/src/direct-sq* path=$(echo $PWD/release/main/msi/*imple*.msi | sed 's#/\([a-z]\)#\1:#' | sed 's#/#\\#g') echo "package_path=$path" >> $GITHUB_OUTPUT @@ -605,7 +710,7 @@ jobs: release-nodejs-libs: runs-on: ubuntu-latest - needs: [build-linux, build-macos] + needs: [build-linux, build-linux-postgres, build-macos] if: startsWith(github.ref, 'refs/tags/v') && (!cancelled()) steps: - name: Checkout current repository @@ -614,6 +719,13 @@ jobs: - name: Install packages for archiving run: sudo apt install -y msitools gcc-mingw-w64 + - name: Download postgres libs artifact + if: needs.build-linux-postgres.result == 'success' + uses: actions/download-artifact@v4 + with: + name: simplex-libs-linux-postgres-x86_64 + path: ${{ runner.temp }}/postgres-libs + - name: Build archives run: | INIT_DIR='${{ runner.temp }}/artifacts' @@ -670,6 +782,17 @@ jobs: zip -r "${PREFIX}-windows-x86_64.zip" libs mv "${PREFIX}-windows-x86_64.zip" "$RELEASE_DIR" && cd "$INIT_DIR" + # Linux PostgreSQL (only if postgres build succeeded) + # ------------------------------------------------- + POSTGRES_LIBS='${{ runner.temp }}/postgres-libs' + if [ -d "$POSTGRES_LIBS" ]; then + mkdir -p linux-postgres/libs + cp "${POSTGRES_LIBS}"/*.so linux-postgres/libs/ + cd linux-postgres + zip -r "${PREFIX}-linux-x86_64-postgres.zip" libs + mv "${PREFIX}-linux-x86_64-postgres.zip" "$RELEASE_DIR" && cd "$INIT_DIR" + fi + - name: Create release in libs repo and upload artifacts uses: softprops/action-gh-release@v2 with: diff --git a/PRIVACY.md b/PRIVACY.md index 18e5539726..5713f8e134 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -27,17 +27,17 @@ permalink: /privacy/index.html SimpleX Chat (also referred to as SimpleX) is the first communication network based on a new protocol stack that builds on the same ideas of complete openness and decentralization as email and web, with the focus on providing security and privacy of communications, and without compromising on usability. -SimpleX messaging protocol is the first protocol that has no user profile IDs of any kind, not even random numbers, cryptographic keys or hashes that identify the users. SimpleX apps allow their users to send messages and files via relay server infrastructure. Relay server owners and operators do not have any access to your messages, thanks to double-ratchet end-to-end encryption algorithm (also known as Signal algorithm - do not confuse with Signal protocols or platform) and additional encryption layers, and they also have no access to your profile and contacts - as they do not host user accounts. +SimpleX messaging protocol is the first protocol that has no user profile IDs of any kind, not even random numbers, cryptographic keys or hashes that identify the users. SimpleX apps allow their users to send messages and files via relay server infrastructure. Relay server owners and operators do not have any access to your messages, thanks to quantum-resistant double-ratchet end-to-end encryption algorithm (also known as Signal algorithm - do not confuse with Signal protocols or platform) and additional encryption layers, and they also have no access to your profile and contacts - as they do not host user accounts. Double ratchet algorithm has such important properties as [forward secrecy](/docs/GLOSSARY.md#forward-secrecy), sender [repudiation](/docs/GLOSSARY.md#) and break-in recovery (also known as [post-compromise security](/docs/GLOSSARY.md#post-compromise-security)). -If you believe that any part of this document is not aligned with SimpleX network mission or values, please raise it via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). +If you believe that any part of this document is not aligned with SimpleX network mission or values, please raise it via [email](mailto:chat@simplex.chat) or [chat](https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw). ## Privacy Policy ### General principles -SimpleX network software uses the best industry practices for security and encryption to provide client and server software for secure [end-to-end encrypted](/docs/GLOSSARY.md#end-to-end-encryption) messaging via private connections. This encryption is protected from being compromised by the relays servers, even if they are modified or compromised, via [man-in-the-middle attack](/docs/GLOSSARY.md#man-in-the-middle-attack). +SimpleX network software uses established industry practices for security and encryption to provide secure [end-to-end encrypted](/docs/GLOSSARY.md#end-to-end-encryption) messaging via private connections. This encryption is protected from [man-in-the-middle attack](/docs/GLOSSARY.md#man-in-the-middle-attack) by the relay servers, even if they are modified or compromised. SimpleX software is built on top of SimpleX messaging and application protocols, based on a new message routing protocol allowing to establish private connections without having identifiers assigned to its users - it does not use emails, phone numbers, usernames, identity keys or any other user profile identifiers to pass messages between the user applications. @@ -45,7 +45,7 @@ SimpleX software is similar in its design approach to email clients and browsers SimpleX network operators are not communication service provider, and provide public relays "as is", as experimental, without any guarantees of availability or data retention. The operators of the relay servers preset in the app ("Preset Server Operators"), including SimpleX Chat Ltd, are committed to maintain a high level of availability, reliability and security. SimpleX client apps can have multiple preset relay server operators that you can opt-in or opt-out of using. You are and will continue to be able to use any other operators or your own servers. -SimpleX network design is based on the principles of users and data sovereignty, and device and operator portability. +SimpleX network design is based on the principles of user and data sovereignty, and device and operator portability. The implementation security assessment of SimpleX cryptography and networking was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2 – see [the announcement](/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). @@ -69,22 +69,26 @@ Your message history is stored only on your own device and the devices of your c #### Private message delivery -You do not have control over which servers are used to send messages to your contacts - these servers are chosen by your contacts. To send messages your client by default uses configured servers to forward messages to the destination servers, thus protecting your IP address from the servers chosen by your contacts. +You do not have control over which servers are used to receive messages by your contacts - these servers are chosen by your contacts. To send messages your client by default uses configured servers to forward messages to the destination servers, thus protecting your IP address from the servers chosen by your contacts. In case you use preset servers of more than one operator, the app will prefer to use a server of an operator different from the operator of the destination server to forward messages, preventing destination server to correlate messages as belonging to one client. +Preset servers do not log IP addresses of the user devices that connect to them. + You can additionally use VPN or some overlay network (e.g., Tor) to hide your IP address from the servers chosen by you. *Please note*: the clients allow changing configuration to connect to the destination servers directly. It is not recommended - if you make such change, your IP address will be visible to the destination servers. #### Storage of messages and files on the servers -The messages are removed from the relay servers as soon as all messages of the file they were stored in are delivered and saving new messages switches to another file, as long as these servers use unmodified published code. Undelivered messages are also marked as delivered after the time that is configured in the messaging servers you use (21 days for preset messaging servers). +The messages stored on the servers are end-to-end encrypted, and cannot be read by server owners. + +The messages are irreversibly removed from the preset relay servers as soon as they are delivered or after 21 days. + +Other relay servers may use message logs that would result in longer storage of delivered messages, until the log file is rotated, which normally should happen within one month if servers use the same code as preset servers. The files are stored on file relay servers for the time configured in the relay servers you use (48 hours for preset file servers). -The encrypted messages can be stored for some time after they are delivered or expired (because servers use append-only logs for message storage). This time varies, and may be longer in connections with fewer messages, but it is usually limited to 1 month, including any backup storage. - #### Connections with other users When you create a connection with another user, two messaging queues (you can think about them as mailboxes) are created on messaging relay servers (chosen by you and your contact each), that can be the preset servers or the servers that you and your contact configured in the app. SimpleX messaging protocol uses separate queues for direct and response messages, and the apps prefer to create these queues on two different relay servers, or, if available, the relays of two different operators, for increased privacy, in case you have more than one relay server configured in the app, which is the default. @@ -93,15 +97,11 @@ Preset and unmodified SimpleX relay servers do not store information about which #### Connection links privacy -When you create a connection with another user, the app generates a link/QR code that can be shared with the user to establish the connection via any channel (email, any other messenger, or a video call). This link is safe to share via insecure channels, as long as you can identify the recipient and also trust that this channel did not replace this link (to mitigate the latter risk you can validate the security code via the app). +When you create a connection with another user, the app generates a one-time link/QR code that can be shared with the user to establish the connection via any channel (email, any other messenger, or a video call). This link is safe to share via insecure channels, as long as you can identify the recipient and also trust that this channel did not replace this link (to mitigate the latter risk you can validate the security code via the app). -While the connection "links" contain SimpleX Chat Ltd domain name `simplex.chat`, this site is never accessed by the app, and is only used for these purposes: -- to direct the new users to the app download instructions, -- to show connection QR code that can be scanned via the app, -- to "namespace" these links, -- to open links directly in the installed app when it is clicked outside of the app. +The connection link contains the address of the server used to establish the connection. Your profile name and picture are stored on this server in encrypted form until your contact uses the link, after which this data is removed. The server cannot access this data without the link. -You can always safely replace the initial part of the link `https://simplex.chat/` either with `simplex:/` (which is a URI scheme provisionally registered with IANA) or with any other domain name where you can self-host the app download instructions and show the connection QR code (but in case it is your domain, it will not open in the app). Also, while the page renders QR code, all the information needed to render it is only available to the browser, as the part of the "link" after `#` symbol is not sent to the website server. +The old connection "links" contained SimpleX Chat Ltd domain name `simplex.chat`, but this site is never accessed by the app - you could replace the initial part of the old link `https://simplex.chat/` either with `simplex:/` or with any other domain name. #### iOS Push Notifications @@ -117,11 +117,15 @@ You can read more about the design of iOS push notifications [here](./blog/20220 Additional technical information can be stored on the network servers, including randomly generated authentication tokens, keys, push tokens, and other material that is necessary to transmit messages. SimpleX network design limits this additional technical information to the minimum required to operate the software and servers. To prevent server overloading or attacks, the servers can temporarily store data that can link to particular users or devices, including IP addresses, geographic location, or information related to the transport sessions. This information is not stored for the absolute majority of the app users, even for those who use the servers very actively. +Because SimpleX servers do not create user accounts and do not store any identifiers linking transport data to message queues or user profiles, this technical data cannot be used by server operators to identify any person. + #### SimpleX Directory -This section applies only to the experimental group directory operated by SimpleX Chat Ltd. +This section applies only to the experimental group directory chat bot operated by SimpleX Chat Ltd. -[SimpleX Directory](/docs/DIRECTORY.md) stores: your search requests, the messages and the members profiles in the registered groups. You can connect to SimpleX Directory via [this address](https://simplex.chat/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). +[SimpleX Directory](/docs/DIRECTORY.md) bot stores: your search requests, the messages and the members profiles in the registered groups. You can connect to SimpleX Directory via [this address](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok). + +You can also view the groups registered in SimpleX directory via the browser at [simplex.chat/directory](https://simplex.chat/directory) #### Public groups and content channels @@ -131,15 +135,23 @@ You may participate in a public group and receive content from a public channel - to retain a copy of your messages according to the Group settings (e.g., the Group may allow irreversible message deletion from the recipient devices for a limited period of time, or it may only allow to edit and mark messages as deleted on recipient devices). Deleting message from the recipient devices or marking message as deleted revokes the license to share the message. - to Group owners: to share your messages with the new Group members as history of the Group. Currently, the Group history shared with the new members is limited to 100 messages. -Group owners may use chat relays or automated bots (Chat Relays) to re-broadcast member messages to all members, for efficiency. The Chat Relays may be operated by the group owners, by preset operators or by 3rd parties. The Chat Relays have access to and will retain messages in line with Group settings, for technical functioning of the Group. Neither you nor group owners grant any content license to Chat Relay operators. +#### Public channels and chat relays (beta) + +Public channels are experimental - their functionality and privacy properties may change. + +Channel owners use chat relays that retain messages to deliver them to channel subscribers. The chat relays may be operated by the channel owners, by preset operators or by 3rd parties. The chat relays are client applications on SimpleX network - they cannot identify subscribers. Neither you nor channel owners grant any content license to chat relay operators. + +#### Public contact, group and channel addresses + +Public addresses contain profile name, picture and other profile details. This data is encrypted on the servers, and can only be accessed via the address. Server operators cannot list addresses and cannot access this data without having the address. Public address data remains on the servers until removed by the user via the app. If you lose access to the app without a backup, server operators have no way to verify address ownership and can only remove addresses following due process. #### User Support -The app includes support contact operated by SimpleX Chat Ltd. If you contact support, any personal data you share is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support [via chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion) when it is possible, and avoid sharing any personal information. +The app includes support contact operated by SimpleX Chat Ltd. If you contact support, any personal data you share is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support [via chat](https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw) when it is possible, and avoid sharing any personal information. ### Preset Server Operators -Preset server operators will not share the information on their servers with each other, other than aggregate usage statistics. +Preset server operators will not share the information or any metadata on their servers with each other, other than aggregate usage statistics. Preset server operators must not provide general access to their servers or the data on their servers to each other. @@ -149,7 +161,7 @@ Preset server operators will provide non-administrative access to control port o The preset server operators use third parties. While they do not have access and cannot share any user data, these third parties may access the encrypted user messages (but NOT the actual unencrypted message content or size) as it is stored or transmitted via the servers. Hosting and network providers can also store IP addresses and other transport information as part of their logs. -SimpleX Chat Ltd uses a third party for email services - if you ask for support via email, your and SimpleX Chat Ltd email providers may access these emails according to their privacy policies and terms. When the request is sensitive, please contact us via SimpleX Chat apps or using encrypted email using PGP key published at [openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat). +SimpleX Chat Ltd uses a third party for email services - if you ask for support via email, your and SimpleX Chat Ltd email providers may access these emails according to their privacy policies and terms. When the request is sensitive, please contact us via SimpleX Chat apps or using encrypted email using PGP key published at [openpgp.org](https://keys.openpgp.org/vks/v1/by-fingerprint/FB44AF81A45BDE327319797C85107E357D4A17FC). The cases when the preset server operators may share the data temporarily stored on the servers: @@ -158,9 +170,7 @@ The cases when the preset server operators may share the data temporarily stored - To detect, prevent, or otherwise address fraud, security, or technical issues. - To protect against harm to the rights, property, or safety of software users, operators of preset servers, or the public as required or permitted by law. -By the time of updating this document, the preset server operators were not served with any enforceable requests and did not provide any information from the servers to any third parties. If the preset server operators are ever requested to provide such access or information, they will follow the due legal process to limit any information shared with the third parties to the minimally required by law. - -Preset server operators will publish information they are legally allowed to share about such requests in the [Transparency reports](./docs/TRANSPARENCY.md). +Reports on requests for user data are published in [Transparency reports](./docs/TRANSPARENCY.md). To date, no user information was provided in response to any requests. If the preset server operators are ever required to provide information, they will follow the due legal process to limit any information shared to the minimally required by law. ### Source code license @@ -168,6 +178,8 @@ As this software is fully open-source and provided under AGPLv3 license, all inf In addition to the AGPLv3 license terms, the preset relay server operators are committed to the software users that these servers will always be compiled from the [published open-source code](https://github.com/simplex-chat/simplexmq), without any modifications. +Users can independently [reproduce builds](./docs/REPRODUCE.md) to verify that the published client and server binaries were compiled from the published code. + ### Updates This Privacy Policy applies to SimpleX Chat Ltd and all other preset server operators you use in the app. @@ -176,7 +188,7 @@ This Privacy Policy may be updated as needed so that it is current, accurate, an Please also read The Conditions of Use of Software and Infrastructure below. -If you have questions about this Privacy Policy please contact SimpleX Chat Ltd via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). +If you have questions about this Privacy Policy or data protection please contact SimpleX Chat Ltd (company number 13691484, registered at 20-22 Wenlock Road, London, United Kingdom N1 7GU) via [email](mailto:chat@simplex.chat) or [chat](https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw). ## Conditions of Use of Software and Infrastructure @@ -188,6 +200,8 @@ You accept the Conditions of Use of Software and Infrastructure ("Conditions") b **Client applications**. SimpleX Chat client application Software (referred to as "app" or "apps") also has no modifications compared with published open-source code, and any developers of the alternative client apps based on SimpleX Chat code are required to publish any modifications under the same AGPLv3 license. Client applications should not include any tracking or analytics code, and do not share any tracking information with SimpleX Chat Ltd, preset server operators or any other third parties. If you ever discover any tracking or analytics code, please report it to SimpleX Chat Ltd, so it can be removed. +Client applications must not include any code that could compromise the security of end-to-end encryption of files and messages. Client applications must not send anything not directly required for users communications without explicit users' consent. + **Accessing the infrastructure**. For the efficiency of the network access, the client Software by default accesses all queues your app creates on any relay server within one user profile via the same network (TCP/IP) connection. At the cost of additional traffic this configuration can be changed to use different transport session for each connection. Relay servers do not collect information about which queues were created or accessed via the same connection, so the relay servers cannot establish which queues belong to the same user profile. Whoever might observe your network traffic would know which relay servers you use, and how much data you send, but not to whom it is sent - the data that leaves the servers is always different from the data they receive - there are no identifiers or ciphertext in common, even inside TLS encryption layer. Please refer to the [technical design document](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information about the privacy model and known security and privacy risks. **Privacy of user data**. Servers do not retain any data you transmit for any longer than necessary to deliver the messages between apps. Preset server operators collect aggregate statistics across all their servers, as supported by published code and can be enabled by any infrastructure operator, but not any statistics per-user, or per geographic location, or per IP address, or per transport session. SimpleX Chat Ltd does not have information about how many people use SimpleX Chat applications, it only knows an approximate number of app installations and the aggregate traffic through the preset servers. In any case, preset server operators do not and will not sell or in any way monetize user data. The future business model assumes charging for some optional Software features instead, in a transparent and fair way. @@ -243,4 +257,4 @@ You accept the Conditions of Use of Software and Infrastructure ("Conditions") b **Ending these conditions**. You may end these Conditions with SimpleX Chat Ltd and preset server operators at any time by deleting the Applications from your devices and discontinuing use of the Infrastructure of SimpleX Chat Ltd and preset server operators. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the conditions, Enforcing the conditions, and Ending these conditions will survive termination of your relationship with SimpleX Chat Ltd and/or preset server operators. -Updated March 3, 2025 +Updated April 18, 2026 diff --git a/README.md b/README.md index 818ed7142f..252fc95708 100644 --- a/README.md +++ b/README.md @@ -425,9 +425,9 @@ Please do NOT report security vulnerabilities via GitHub issues. ## License -This software is licensed under the GNU Affero General Public License version 3 (AGPLv3). See the [LICENSE](./LICENSE) file for details. The SimpleX and SimpleX Chat name, logo, and associated branding materials are not covered by this license and are subject to the terms outlined in the [TRADEMARK](./docs/TRADEMARK.md) file. +This software is licensed under the GNU Affero General Public License version 3 (AGPLv3). See the [LICENSE](./LICENSE) file for details. The SimpleX and SimpleX Chat name, logo, associated branding materials, and application and website graphic assets (illustrations, images, visual designs, etc.) are not covered by this license and are subject to the terms outlined in the [TRADEMARK](./docs/TRADEMARK.md) and [ASSETS_LICENSE](./assets/ASSETS_LICENSE.md) files respectively. -Graphic designs, artworks and layouts are not licensed for re-use. If you want to use them in your publications, please ask for permission. Texts can be used as direct quotes, referencing the source. +If you want to use any graphic assets in your publications, please ask for permission. Texts can be used as direct quotes, referencing the source. [iOS app](https://apps.apple.com/us/app/simplex-chat/id1605771084)   diff --git a/apps/ios/.gitignore b/apps/ios/.gitignore index 3d152a0610..ea8e911891 100644 --- a/apps/ios/.gitignore +++ b/apps/ios/.gitignore @@ -69,3 +69,7 @@ Libraries/ Shared/MyPlayground.playground/* testpush.sh + +# Local build config and generated assets +Local.xcconfig +Shared/SimpleXAssets.xcassets/*.imageset diff --git a/apps/ios/Debug.xcconfig b/apps/ios/Debug.xcconfig new file mode 100644 index 0000000000..7f0389c760 --- /dev/null +++ b/apps/ios/Debug.xcconfig @@ -0,0 +1,2 @@ +SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; +#include? "Local.xcconfig" diff --git a/apps/ios/README.md b/apps/ios/README.md index fb6a6ed40d..1e987f655e 100644 --- a/apps/ios/README.md +++ b/apps/ios/README.md @@ -85,6 +85,27 @@ Workflow: - `Product > Export Localizations` - Export XLIFF files - `Product > Import Localizations` - Import updated translations +## SimpleX Assets + +The app includes optional assets behind the `SIMPLEX_ASSETS` Swift compilation flag. Without setup, the app builds normally without them. + +### Setup + +Create `Local.xcconfig` (gitignored) in the `apps/ios/` directory: +``` +SIMPLEX_ASSETS_DIR = /path/to/assets +SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) SIMPLEX_ASSETS +``` + +The copy script (`scripts/ios/copy-assets.sh`) runs as a build phase on each build but exits immediately if `SIMPLEX_ASSETS` is not set. + +### Updating assets + +When source images change, regenerate resized images (requires ImageMagick): +```bash +cd path/to/assets && ./resize.sh +``` + ## Background Capabilities Configured in Info.plist: diff --git a/apps/ios/Release.xcconfig b/apps/ios/Release.xcconfig new file mode 100644 index 0000000000..234f81e782 --- /dev/null +++ b/apps/ios/Release.xcconfig @@ -0,0 +1 @@ +#include? "Local.xcconfig" diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 1131069d88..547c2b7000 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -62,6 +62,7 @@ enum ChatCommand: ChatCmdProtocol { case apiGetReactionMembers(userId: Int64, groupId: Int64, itemId: Int64, reaction: MsgReaction) case apiPlanForwardChatItems(fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64]) case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?) + case apiShareChatMsgContent(shareChatType: ChatType, shareChatId: Int64, toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool) case apiGetNtfToken case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) case apiVerifyToken(token: DeviceToken, nonce: String, code: String) @@ -72,6 +73,7 @@ enum ChatCommand: ChatCmdProtocol { case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile) case apiNewPublicGroup(userId: Int64, incognito: Bool, relayIds: [Int64], groupProfile: GroupProfile) case apiGetGroupRelays(groupId: Int64) + case apiAddGroupRelays(groupId: Int64, relayIds: [Int64]) case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) case apiJoinGroup(groupId: Int64) case apiAcceptMember(groupId: Int64, groupMemberId: Int64, memberRole: GroupMemberRole) @@ -128,7 +130,7 @@ enum ChatCommand: ChatCmdProtocol { case apiAddContact(userId: Int64, incognito: Bool) case apiSetConnectionIncognito(connId: Int64, incognito: Bool) case apiChangeConnectionUser(connId: Int64, userId: Int64) - case apiConnectPlan(userId: Int64, connLink: String) + case apiConnectPlan(userId: Int64, connLink: String, linkOwnerSig: LinkOwnerSig?) case apiPrepareContact(userId: Int64, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData) case apiPrepareGroup(userId: Int64, connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData) case apiChangePreparedContactUser(contactId: Int64, newUserId: Int64) @@ -261,6 +263,9 @@ enum ChatCommand: ChatCmdProtocol { let ttlStr = ttl != nil ? "\(ttl!)" : "default" let asGroup = sendAsGroup ? " as_group=on" : "" return "/_forward \(ref(toChatType, toChatId, scope: toScope))\(asGroup) \(ref(fromChatType, fromChatId, scope: fromScope)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)" + case let .apiShareChatMsgContent(shareChatType, shareChatId, toChatType, toChatId, toScope, sendAsGroup): + let asGroup = sendAsGroup ? "(as_group=on)" : "" + return "/_share chat content \(ref(shareChatType, shareChatId, scope: nil)) \(ref(toChatType, toChatId, scope: toScope))\(asGroup)" case .apiGetNtfToken: return "/_ntf get " case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" @@ -271,6 +276,7 @@ enum ChatCommand: ChatCmdProtocol { case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))" case let .apiNewPublicGroup(userId, incognito, relayIds, groupProfile): return "/_public group \(userId) incognito=\(onOff(incognito)) \(relayIds.map(String.init).joined(separator: ",")) \(encodeJSON(groupProfile))" case let .apiGetGroupRelays(groupId): return "/_get relays #\(groupId)" + case let .apiAddGroupRelays(groupId, relayIds): return "/_add relays #\(groupId) \(relayIds.map(String.init).joined(separator: ","))" case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)" case let .apiJoinGroup(groupId): return "/_join #\(groupId)" case let .apiAcceptMember(groupId, groupMemberId, memberRole): return "/_accept member #\(groupId) \(groupMemberId) \(memberRole.rawValue)" @@ -337,7 +343,9 @@ enum ChatCommand: ChatCmdProtocol { case let .apiAddContact(userId, incognito): return "/_connect \(userId) incognito=\(onOff(incognito))" case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))" case let .apiChangeConnectionUser(connId, userId): return "/_set conn user :\(connId) \(userId)" - case let .apiConnectPlan(userId, connLink): return "/_connect plan \(userId) \(connLink)" + case let .apiConnectPlan(userId, connLink, linkOwnerSig): + let sigStr = if let linkOwnerSig { " sig=\(encodeJSON(linkOwnerSig))" } else { "" } + return "/_connect plan \(userId) \(connLink)\(sigStr)" case let .apiPrepareContact(userId, connLink, contactShortLinkData): return "/_prepare contact \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") \(encodeJSON(contactShortLinkData))" case let .apiPrepareGroup(userId, connLink, directLink, groupShortLinkData): return "/_prepare group \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") direct=\(onOff(directLink)) \(encodeJSON(groupShortLinkData))" case let .apiChangePreparedContactUser(contactId, newUserId): return "/_set contact user @\(contactId) \(newUserId)" @@ -451,6 +459,7 @@ enum ChatCommand: ChatCmdProtocol { case .apiGetReactionMembers: return "apiGetReactionMembers" case .apiPlanForwardChatItems: return "apiPlanForwardChatItems" case .apiForwardChatItems: return "apiForwardChatItems" + case .apiShareChatMsgContent: return "apiShareChatMsgContent" case .apiGetNtfToken: return "apiGetNtfToken" case .apiRegisterToken: return "apiRegisterToken" case .apiVerifyToken: return "apiVerifyToken" @@ -461,6 +470,7 @@ enum ChatCommand: ChatCmdProtocol { case .apiNewGroup: return "apiNewGroup" case .apiNewPublicGroup: return "apiNewPublicGroup" case .apiGetGroupRelays: return "apiGetGroupRelays" + case .apiAddGroupRelays: return "apiAddGroupRelays" case .apiAddMember: return "apiAddMember" case .apiJoinGroup: return "apiJoinGroup" case .apiAcceptMember: return "apiAcceptMember" @@ -821,6 +831,7 @@ enum ChatResponse1: Decodable, ChatAPIResult { case acceptingContactRequest(user: UserRef, contact: Contact) case contactRequestRejected(user: UserRef, contactRequest: UserContactRequest, contact_: Contact?) case newChatItems(user: UserRef, chatItems: [AChatItem]) + case chatMsgContent(user: UserRef, msgContent: MsgContent) case groupChatItemsDeleted(user: UserRef, groupInfo: GroupInfo, chatItemIDs: Set, byUser: Bool, member_: GroupMember?) case forwardPlan(user: UserRef, chatItemIds: [Int64], forwardConfirmation: ForwardConfirmation?) case chatItemUpdated(user: UserRef, chatItem: AChatItem) @@ -864,6 +875,7 @@ enum ChatResponse1: Decodable, ChatAPIResult { case .acceptingContactRequest: "acceptingContactRequest" case .contactRequestRejected: "contactRequestRejected" case .newChatItems: "newChatItems" + case .chatMsgContent: "chatMsgContent" case .groupChatItemsDeleted: "groupChatItemsDeleted" case .forwardPlan: "forwardPlan" case .chatItemUpdated: "chatItemUpdated" @@ -898,6 +910,7 @@ enum ChatResponse1: Decodable, ChatAPIResult { case let .newChatItems(u, chatItems): let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") return withUser(u, itemsString) + case let .chatMsgContent(u, mc): return withUser(u, String(describing: mc)) case let .groupChatItemsDeleted(u, gInfo, chatItemIDs, byUser, member_): return withUser(u, "chatItemIDs: \(String(describing: chatItemIDs))\ngroupInfo: \(String(describing: gInfo))\nbyUser: \(byUser)\nmember_: \(String(describing: member_))") case let .forwardPlan(u, chatItemIds, forwardConfirmation): return withUser(u, "items: \(chatItemIds) forwardConfirmation: \(String(describing: forwardConfirmation))") @@ -932,7 +945,10 @@ enum ChatResponse2: Decodable, ChatAPIResult { // group responses case groupCreated(user: UserRef, groupInfo: GroupInfo) case publicGroupCreated(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink, groupRelays: [GroupRelay]) + case publicGroupCreationFailed(user: UserRef, addRelayResults: [AddRelayResult]) case groupRelays(user: UserRef, groupInfo: GroupInfo, groupRelays: [GroupRelay]) + case groupRelaysAdded(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink, groupRelays: [GroupRelay]) + case groupRelaysAddFailed(user: UserRef, addRelayResults: [AddRelayResult]) case sentGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, member: GroupMember) case userAcceptedGroupSent(user: UserRef, groupInfo: GroupInfo, hostContact: Contact?) case userDeletedMembers(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], withMessages: Bool) @@ -984,7 +1000,10 @@ enum ChatResponse2: Decodable, ChatAPIResult { switch self { case .groupCreated: "groupCreated" case .publicGroupCreated: "publicGroupCreated" + case .publicGroupCreationFailed: "publicGroupCreationFailed" case .groupRelays: "groupRelays" + case .groupRelaysAdded: "groupRelaysAdded" + case .groupRelaysAddFailed: "groupRelaysAddFailed" case .sentGroupInvitation: "sentGroupInvitation" case .userAcceptedGroupSent: "userAcceptedGroupSent" case .userDeletedMembers: "userDeletedMembers" @@ -1032,7 +1051,10 @@ enum ChatResponse2: Decodable, ChatAPIResult { switch self { case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .publicGroupCreated(u, groupInfo, groupLink, groupRelays): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)\ngroupRelays: \(groupRelays)") + case let .publicGroupCreationFailed(u, addRelayResults): return withUser(u, "addRelayResults: \(addRelayResults)") case let .groupRelays(u, groupInfo, groupRelays): return withUser(u, "groupInfo: \(groupInfo)\ngroupRelays: \(groupRelays)") + case let .groupRelaysAdded(u, groupInfo, groupLink, groupRelays): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)\ngroupRelays: \(groupRelays)") + case let .groupRelaysAddFailed(u, addRelayResults): return withUser(u, "addRelayResults: \(addRelayResults)") case let .sentGroupInvitation(u, groupInfo, contact, member): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)") case let .userAcceptedGroupSent(u, groupInfo, hostContact): return withUser(u, "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))") case let .userDeletedMembers(u, groupInfo, members, withMessages): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\nwithMessages: \(withMessages)") @@ -1341,6 +1363,11 @@ enum ChatPagination { } } +enum OwnerVerification: Decodable, Hashable { + case verified + case failed(reason: String) +} + enum ConnectionPlan: Decodable, Hashable { case invitationLink(invitationLinkPlan: InvitationLinkPlan) case contactAddress(contactAddressPlan: ContactAddressPlan) @@ -1349,14 +1376,14 @@ enum ConnectionPlan: Decodable, Hashable { } enum InvitationLinkPlan: Decodable, Hashable { - case ok(contactSLinkData_: ContactShortLinkData?) + case ok(contactSLinkData_: ContactShortLinkData?, ownerVerification: OwnerVerification?) case ownLink case connecting(contact_: Contact?) case known(contact: Contact) } enum ContactAddressPlan: Decodable, Hashable { - case ok(contactSLinkData_: ContactShortLinkData?) + case ok(contactSLinkData_: ContactShortLinkData?, ownerVerification: OwnerVerification?) case ownLink case connectingConfirmReconnect case connectingProhibit(contact: Contact) @@ -1371,11 +1398,12 @@ public struct GroupShortLinkInfo: Decodable, Hashable { } enum GroupLinkPlan: Decodable, Hashable { - case ok(groupSLinkInfo_: GroupShortLinkInfo?, groupSLinkData_: GroupShortLinkData?) + case ok(groupSLinkInfo_: GroupShortLinkInfo?, groupSLinkData_: GroupShortLinkData?, ownerVerification: OwnerVerification?) case ownLink(groupInfo: GroupInfo) case connectingConfirmReconnect case connectingProhibit(groupInfo_: GroupInfo?) case known(groupInfo: GroupInfo) + case noRelays(groupSLinkData_: GroupShortLinkData?) } struct ChatTagData: Encodable { @@ -1965,6 +1993,11 @@ struct RelayConnectionResult: Decodable { var relayError: ChatError? } +struct AddRelayResult: Decodable { + var relay: UserChatRelay + var relayError: ChatError? +} + enum ProtocolTestStep: String, Decodable, Equatable { case connect case disconnect diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 9c23ac6307..a1d28b8e22 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -344,9 +344,12 @@ class ChannelRelaysModel: ObservableObject { } func updateRelay(_ groupInfo: GroupInfo, _ relay: GroupRelay) { - if groupId == groupInfo.groupId, - let i = groupRelays.firstIndex(where: { $0.groupRelayId == relay.groupRelayId }) { - groupRelays[i] = relay + if groupId == groupInfo.groupId { + if let i = groupRelays.firstIndex(where: { $0.groupRelayId == relay.groupRelayId }) { + groupRelays[i] = relay + } else { + groupRelays.append(relay) + } } } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index e527df1abd..ea2de31569 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -458,7 +458,7 @@ func apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTa func apiGetChatContentTypes(chatId: ChatId, scope: GroupChatScope? = nil) async throws -> [MsgContentTag] { let r: ChatResponse0 = try await chatSendCmd(.apiGetChatContentTypes(chatId: chatId, scope: scope)) - if case let .chatContentTypes(types) = r { return types } + if case let .chatContentTypes(types) = r { return types.filter { if case .unknown = $0 { return false }; return true } } throw r.unexpected } @@ -503,6 +503,12 @@ func apiPlanForwardChatItems(type: ChatType, id: Int64, scope: GroupChatScope?, throw r.unexpected } +func apiShareChatMsgContent(shareChatType: ChatType, shareChatId: Int64, toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool) async throws -> MsgContent { + let r: ChatResponse1 = try await chatSendCmd(.apiShareChatMsgContent(shareChatType: shareChatType, shareChatId: shareChatId, toChatType: toChatType, toChatId: toChatId, toScope: toScope, sendAsGroup: sendAsGroup)) + if case let .chatMsgContent(_, mc) = r { return mc } + throw r.unexpected +} + func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool = false, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? { let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, toScope: toScope, sendAsGroup: sendAsGroup, fromChatType: fromChatType, fromChatId: fromChatId, fromScope: fromScope, itemIds: itemIds, ttl: ttl) return await processSendMessageCmd(toChatType: toChatType, cmd: cmd) @@ -1020,12 +1026,12 @@ func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> Pendi if let r { throw r.unexpected } else { return nil } } -func apiConnectPlan(connLink: String, inProgress: BoxedValue) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) { +func apiConnectPlan(connLink: String, linkOwnerSig: LinkOwnerSig? = nil, inProgress: BoxedValue) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnectPlan: no current user") return (nil, nil) } - let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink), inProgress: inProgress) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink, linkOwnerSig: linkOwnerSig), inProgress: inProgress) if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return ((connLink, connPlan), nil) } let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil } return (nil, alert) @@ -1111,6 +1117,27 @@ private func apiConnectResponseAlert(_ r: APIResult) -> Alert { } } +func connErrorText(_ e: ChatError) -> String { + switch e { + case .error(.invalidConnReq): + NSLocalizedString("Invalid connection link", comment: "conn error description") + case .error(.unsupportedConnReq): + NSLocalizedString("Unsupported connection link", comment: "conn error description") + case .errorAgent(.SMP(_, .AUTH)): + NSLocalizedString("Connection error (AUTH)", comment: "conn error description") + case let .errorAgent(.SMP(_, .BLOCKED(info))): + NSLocalizedString("Connection blocked: \(info.reason.text)", comment: "conn error description") + case .errorAgent(.SMP(_, .QUOTA)): + NSLocalizedString("The connection reached the limit of undelivered messages", comment: "conn error description") + default: + if getNetworkErrorAlert(e) != nil { + NSLocalizedString("Network error", comment: "conn error description") + } else { + "\(NSLocalizedString("Error", comment: "conn error description")): \(responseError(e))" + } + } +} + func contactAlreadyExistsAlert(_ contact: Contact) -> Alert { mkAlert( title: "Contact already exists", @@ -1841,12 +1868,19 @@ func apiNewGroup(incognito: Bool, groupProfile: GroupProfile) throws -> GroupInf throw r.unexpected } -func apiNewPublicGroup(incognito: Bool, relayIds: [Int64], groupProfile: GroupProfile) async throws -> (GroupInfo, GroupLink, [GroupRelay])? { +enum PublicGroupCreationResult { + case created(GroupInfo, GroupLink, [GroupRelay]) + case creationFailed([AddRelayResult]) +} + +func apiNewPublicGroup(incognito: Bool, relayIds: [Int64], groupProfile: GroupProfile) async throws -> PublicGroupCreationResult? { let userId = try currentUserId("apiNewPublicGroup") let r: APIResult? = await chatApiSendCmdWithRetry(.apiNewPublicGroup(userId: userId, incognito: incognito, relayIds: relayIds, groupProfile: groupProfile)) switch r { case let .result(.publicGroupCreated(_, groupInfo, groupLink, groupRelays)): - return (groupInfo, groupLink, groupRelays) + return .created(groupInfo, groupLink, groupRelays) + case let .result(.publicGroupCreationFailed(_, addRelayResults)): + return .creationFailed(addRelayResults) default: if let r { throw r.unexpected } else { return nil } } } @@ -1857,6 +1891,22 @@ func apiGetGroupRelays(_ groupId: Int64) async -> [GroupRelay] { return [] } +enum AddGroupRelaysResult { + case added(GroupInfo, GroupLink, [GroupRelay]) + case addFailed([AddRelayResult]) +} + +func apiAddGroupRelays(_ groupId: Int64, relayIds: [Int64]) async throws -> AddGroupRelaysResult? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiAddGroupRelays(groupId: groupId, relayIds: relayIds)) + switch r { + case let .result(.groupRelaysAdded(_, groupInfo, groupLink, groupRelays)): + return .added(groupInfo, groupLink, groupRelays) + case let .result(.groupRelaysAddFailed(_, addRelayResults)): + return .addFailed(addRelayResults) + default: if let r { throw r.unexpected } else { return nil } + } +} + func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember { let r: ChatResponse2 = try await chatSendCmd(.apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole)) if case let .sentGroupInvitation(_, _, _, member) = r { return member } @@ -2149,7 +2199,7 @@ func startChat(refreshInvitations: Bool = true, onboarding: Bool = false) throws withAnimation { let savedOnboardingStage = onboardingStageDefault.get() m.onboardingStage = [.step1_SimpleXInfo, .step2_CreateProfile].contains(savedOnboardingStage) && m.users.count == 1 - ? .step3_ChooseServerOperators + ? .step4_NetworkCommitments : savedOnboardingStage if m.onboardingStage == .onboardingComplete && !privacyDeliveryReceiptsSet.get() { m.setDeliveryReceipts = true diff --git a/apps/ios/Shared/SimpleXAssets.xcassets/Contents.json b/apps/ios/Shared/SimpleXAssets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/Shared/SimpleXAssets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIChatLinkHeader.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIChatLinkHeader.swift new file mode 100644 index 0000000000..aaaa29929d --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIChatLinkHeader.swift @@ -0,0 +1,68 @@ +import SwiftUI +import SimpleXChat + +struct CIChatLinkHeader: View { + @EnvironmentObject var theme: AppTheme + @Environment(\.showTimestamp) var showTimestamp: Bool + var chatItem: ChatItem + var chatLink: MsgChatLink + var ownerSig: LinkOwnerSig? + var hasText: Bool + + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + + var body: some View { + VStack(alignment: .leading) { + linkProfileView() + .padding(.horizontal, 2) + .padding(.top, 8) + .padding(.bottom, 6) + .overlay(DetermineWidth()) + Divider() + VStack(alignment: .leading, spacing: 2) { + if let descr = chatLink.shortDescription { + Text(descr) + .font(.footnote) + .foregroundColor(theme.colors.secondary) + .lineLimit(2) + .padding(.bottom, 2) + } + Text(chatLink.infoLine(signed: ownerSig != nil)) + .font(.footnote) + .foregroundColor(theme.colors.secondary) + .padding(.bottom, 2) + let t = Text("Tap to open").foregroundColor(theme.colors.primary).font(.callout) + if hasText { + t + } else { + t + + Text(verbatim: " ") + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) + } + } + .overlay(DetermineWidth()) + } + .padding(.horizontal, 12) + .padding(.vertical, 6) + } + + private func linkProfileView() -> some View { + HStack(alignment: .top) { + ProfileImage( + imageStr: chatLink.image, + iconName: chatLink.iconName, + size: 44, + color: Color(uiColor: .tertiaryLabel) + ) + .padding(.trailing, 4) + VStack(alignment: .leading) { + Text(chatLink.displayName).font(.headline).lineLimit(2) + let fn = chatLink.fullName + if fn != "" && fn != chatLink.displayName { + Text(fn).font(.subheadline).lineLimit(2) + } + } + .frame(minHeight: 44) + } + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift index b3fdd3f8e3..4eb187bcac 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -14,6 +14,7 @@ import SimpleXChat struct CILinkView: View { @EnvironmentObject var theme: AppTheme let linkPreview: LinkPreview + let maxWidth: CGFloat @State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0 var body: some View { @@ -21,7 +22,8 @@ struct CILinkView: View { if let uiImage = imageFromBase64(linkPreview.image) { Image(uiImage: uiImage) .resizable() - .aspectRatio(1 / heightRatio(uiImage.size), contentMode: .fill) + .scaledToFill() + .frame(width: maxWidth, height: maxWidth * heightRatio(uiImage.size)) .clipped() .modifier(PrivacyBlur(blurred: $blurred)) .if(!blurred) { v in @@ -116,7 +118,7 @@ struct LargeLinkPreview_Previews: PreviewProvider { description: "", image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z" ) - CILinkView(linkPreview: preview) + CILinkView(linkPreview: preview, maxWidth: 360) .previewLayout(.fixed(width: 360, height: 200)) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index ec8bc852c0..d09289c1d5 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -167,8 +167,18 @@ struct FramedItemView: View { case let .report(text, reason): ciMsgContentView(chatItem, txtPrefix: reason.attrString) case let .link(_, preview): - CILinkView(linkPreview: preview) + CILinkView(linkPreview: preview, maxWidth: maxWidth) ciMsgContentView(chatItem) + case let .chat(text, chatLink, ownerSig): + let hasText = text != chatLink.connLinkStr + CIChatLinkHeader(chatItem: chatItem, chatLink: chatLink, ownerSig: ownerSig, hasText: hasText) + .overlay(DetermineWidth()) + .simultaneousGesture(TapGesture().onEnded { + planAndConnect(chatLink.connLinkStr, linkOwnerSig: ownerSig, theme: theme, dismiss: false) + }) + if hasText { + ciMsgContentView(chatItem, stripLink: chatLink.connLinkStr) + } case let .unknown(_, text: text): if chatItem.file == nil { ciMsgContentView(chatItem) @@ -244,6 +254,11 @@ struct FramedItemView: View { ciQuotedMsgView(qi) .padding(.trailing, 20).frame(minWidth: msgWidth, alignment: .leading) ciQuoteIconView("mic.fill") + case let .chat(text, chatLink, _): + let prefix = NSAttributedString(string: chatLink.displayName + (text != chatLink.connLinkStr ? " - " : "")) + ciQuotedMsgView(qi, stripLink: chatLink.connLinkStr, prefix: prefix) + .padding(.trailing, 20).frame(minWidth: msgWidth, alignment: .leading) + ciQuoteIconView(chatLink.smallIconName) default: ciQuotedMsgView(qi) } @@ -260,7 +275,7 @@ struct FramedItemView: View { } } - private func ciQuotedMsgView(_ qi: CIQuote) -> some View { + private func ciQuotedMsgView(_ qi: CIQuote, stripLink: String? = nil, prefix: NSAttributedString? = nil) -> some View { Group { if let sender = qi.getSender(membership()) { VStack(alignment: .leading, spacing: 2) { @@ -268,10 +283,10 @@ struct FramedItemView: View { .font(.caption) .foregroundColor(qi.chatDir == .groupSnd ? .accentColor : theme.colors.secondary) .lineLimit(1) - ciQuotedMsgTextView(qi, lines: 2) + ciQuotedMsgTextView(qi, lines: 2, stripLink: stripLink, prefix: prefix) } } else { - ciQuotedMsgTextView(qi, lines: 3) + ciQuotedMsgTextView(qi, lines: 3, stripLink: stripLink, prefix: prefix) } } .fixedSize(horizontal: false, vertical: true) @@ -280,8 +295,8 @@ struct FramedItemView: View { } @inline(__always) - private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int) -> some View { - MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText, textStyle: .subheadline) + private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int, stripLink: String? = nil, prefix: NSAttributedString? = nil) -> some View { + MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText, textStyle: .subheadline, prefix: prefix, stripLink: stripLink) .lineLimit(lines) .padding(.bottom, 6) } @@ -303,7 +318,7 @@ struct FramedItemView: View { } } - @ViewBuilder private func ciMsgContentView(_ ci: ChatItem, txtPrefix: NSAttributedString? = nil) -> some View { + @ViewBuilder private func ciMsgContentView(_ ci: ChatItem, txtPrefix: NSAttributedString? = nil, stripLink: String? = nil) -> some View { let text = ci.meta.isLive ? ci.content.msgContent?.text ?? ci.text : ci.text let rtl = isRightToLeft(text) let ft = text == "" ? [] : ci.formattedText @@ -316,7 +331,8 @@ struct FramedItemView: View { mentions: ci.mentions, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, rightToLeft: rtl, - prefix: txtPrefix + prefix: txtPrefix, + stripLink: stripLink ) .environment(\.containerBackground, UIColor(chatItemFrameColor(ci, theme))) .multilineTextAlignment(rtl ? .trailing : .leading) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift index fdf3743aac..d831333c20 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift @@ -77,6 +77,25 @@ struct CIMsgError: View { } } +struct RcvMsgErrorItemView: View { + @ObservedObject var chat: Chat + var rcvMsgError: RcvMsgError + var chatItem: ChatItem + + var body: some View { + CIMsgError(chat: chat, chatItem: chatItem) { + let message: LocalizedStringKey = switch rcvMsgError { + case let .dropped(attempts): "The app removed this message after \(attempts) attempts to receive it." + case let .parseError(parseError): "\(parseError)" + } + AlertManager.shared.showAlertMsg( + title: "Message error", + message: message + ) + } + } +} + struct IntegrityErrorItemView_Previews: PreviewProvider { static var previews: some View { IntegrityErrorItemView(chat: Chat.sampleData, msgError: .msgBadHash, chatItem: ChatItem.getIntegrityErrorSample()) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 77bd41c5b8..2f4338c0af 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -39,6 +39,7 @@ struct MsgContentView: View { var userMemberId: String? = nil var rightToLeft = false var prefix: NSAttributedString? = nil + var stripLink: String? = nil @State private var showSecrets: Set = [] @State private var typingIdx = 0 @State private var timer: Timer? @@ -105,7 +106,8 @@ struct MsgContentView: View { showSecrets: showSecrets, commands: chat.chatInfo.useCommands && chat.chatInfo.sndReady, backgroundColor: containerBackground, - prefix: prefix + prefix: prefix, + stripLink: stripLink ) let s = r.string let t: Text @@ -289,8 +291,11 @@ func messageText( showSecrets: Set?, commands: Bool = false, backgroundColor: UIColor, - prefix: NSAttributedString? = nil + prefix: NSAttributedString? = nil, + stripLink: String? = nil ) -> MsgTextResult { + let text = if let stripLink { stripTextLink(text, stripLink) } else { text } + let formattedText = if let stripLink { stripFormattedTextLink(formattedText, stripLink) } else { formattedText } let res = NSMutableAttributedString() let descr = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle) let font = UIFont.preferredFont(forTextStyle: textStyle) @@ -465,6 +470,24 @@ func viaHost(_ smpHosts: [String]) -> String { "(via \(smpHosts.first ?? "?"))" } +func stripTextLink(_ text: String, _ link: String) -> String { + text == link + ? "" + : text.hasSuffix("\n" + link) + ? String(text.dropLast(link.count + 1)) + : text +} + +func stripFormattedTextLink(_ ft: [FormattedText]?, _ link: String) -> [FormattedText]? { + guard var ft, ft.last?.text == link else { return ft } + ft.removeLast() + if let i = ft.indices.last, ft[i].format == nil, ft[i].text.hasSuffix("\n") { + ft[i].text = String(ft[i].text.dropLast()) + if ft[i].text.isEmpty { ft.removeLast() } + } + return ft.isEmpty ? nil : ft +} + struct MsgContentView_Previews: PreviewProvider { static var previews: some View { let chatItem = ChatItem.getSample(1, .directSnd, .now, "hello") diff --git a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift index dfc620c402..d83a5e8504 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift @@ -14,9 +14,12 @@ struct ChatItemForwardingView: View { @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss - var chatItems: [ChatItem] - var fromChatInfo: ChatInfo - @Binding var composeState: ComposeState + var title: LocalizedStringKey = "Forward" + var chatItems: [ChatItem] = [] + var fromChatInfo: ChatInfo? = nil + var composeState: Binding? = nil + var isProhibited: ((Chat) -> Bool)? = nil + var onSelectChat: ((Chat) -> Void)? = nil @State private var searchText: String = "" @State private var alert: SomeAlert? @@ -32,7 +35,7 @@ struct ChatItemForwardingView: View { } } ToolbarItem(placement: .principal) { - Text("Forward") + Text(title) .bold() } } @@ -71,7 +74,7 @@ struct ChatItemForwardingView: View { } @ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View { - let prohibited = chatItems.map { ci in + let prohibited = isProhibited?(chat) ?? chatItems.map { ci in chat.prohibitedByPref( hasSimplexLink: hasSimplexLink(ci.content.msgContent?.text), isMediaOrFileAttachment: ci.content.msgContent?.isMediaOrFileAttachment ?? false, @@ -88,16 +91,19 @@ struct ChatItemForwardingView: View { ), id: "forward prohibited by preferences" ) - } else { + } else if let onSelectChat { + dismiss() + onSelectChat(chat) + } else if let fromChatInfo, let composeState { dismiss() if chat.id == fromChatInfo.id { - composeState = ComposeState( - message: composeState.message, - preview: composeState.linkPreview != nil ? composeState.preview : .noPreview, + composeState.wrappedValue = ComposeState( + message: composeState.wrappedValue.message, + preview: composeState.wrappedValue.linkPreview != nil ? composeState.wrappedValue.preview : .noPreview, contextItem: .forwardingItems(chatItems: chatItems, fromChatInfo: fromChatInfo) ) } else { - composeState = ComposeState.init(forwardingItems: chatItems, fromChatInfo: fromChatInfo) + composeState.wrappedValue = ComposeState.init(forwardingItems: chatItems, fromChatInfo: fromChatInfo) ItemsModel.shared.loadOpenChat(chat.id) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index f72bf083f6..1839651daa 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -145,6 +145,7 @@ struct ChatItemContentView: View { } else { ZStack {} } + case let .rcvMsgError(rcvMsgError): RcvMsgErrorItemView(chat: chat, rcvMsgError: rcvMsgError, chatItem: chatItem) case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(chat: chat, msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem) case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole) case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole) @@ -171,8 +172,8 @@ struct ChatItemContentView: View { case .rcvBlocked: deletedItemView() case let .sndDirectE2EEInfo(e2eeInfo): CIEventView(eventText: directE2EEInfoText(e2eeInfo)) case let .rcvDirectE2EEInfo(e2eeInfo): CIEventView(eventText: directE2EEInfoText(e2eeInfo)) - case .sndGroupE2EEInfo: CIEventView(eventText: e2eeInfoNoPQText()) - case .rcvGroupE2EEInfo: CIEventView(eventText: e2eeInfoNoPQText()) + case let .sndGroupE2EEInfo(e2eeInfo): CIEventView(eventText: groupE2EEInfoText(e2eeInfo)) + case let .rcvGroupE2EEInfo(e2eeInfo): CIEventView(eventText: groupE2EEInfoText(e2eeInfo)) case .chatBanner: EmptyView() case let .invalidJSON(json): CIInvalidJSONView(json: json) } @@ -195,7 +196,7 @@ struct ChatItemContentView: View { } private func pendingReviewEventItemText() -> Text { - Text(chatItem.content.text) + Text(chatItem.content.text(isChannel: chat.chatInfo.isChannel)) .font(.caption) .foregroundColor(theme.colors.secondary) .fontWeight(.bold) @@ -209,9 +210,9 @@ struct ChatItemContentView: View { .font(.caption) .foregroundColor(secondaryColor) .fontWeight(.light) - + chatEventText(chatItem, secondaryColor) + + chatEventText(chatItem, secondaryColor, isChannel: chat.chatInfo.isChannel) } else { - return chatEventText(chatItem, secondaryColor) + return chatEventText(chatItem, secondaryColor, isChannel: chat.chatInfo.isChannel) } } @@ -234,7 +235,7 @@ struct ChatItemContentView: View { return if count <= 1 { nil } else if ns.count == 0 { - Text("\(count) group events") + chat.chatInfo.isChannel ? Text("\(count) channel events") : Text("\(count) group events") } else if count > ns.count { Text(members) + textSpace + Text("and \(count - ns.count) other events") } else { @@ -256,6 +257,12 @@ struct ChatItemContentView: View { e2eeInfoText("Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.") } + private func groupE2EEInfoText(_ info: E2EEInfo) -> Text { + info.public == true + ? e2eeInfoText("Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages.") + : e2eeInfoNoPQText() + } + private func e2eeInfoText(_ s: LocalizedStringKey) -> Text { Text(s) .font(.caption) @@ -275,8 +282,8 @@ func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text, _ secondaryColor chatEventText(Text(eventText) + textSpace + ts, secondaryColor) } -func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text { - chatEventText("\(ci.content.text)", ci.timestampText, secondaryColor) +func chatEventText(_ ci: ChatItem, _ secondaryColor: Color, isChannel: Bool = false) -> Text { + chatEventText("\(ci.content.text(isChannel: isChannel))", ci.timestampText, secondaryColor) } struct ChatItemView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 1898fd2851..a141a53b4c 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -141,8 +141,7 @@ struct ChatView: View { showCommandsMenu: $showCommandsMenu, keyboardVisible: $keyboardVisible, keyboardHiddenDate: $keyboardHiddenDate, - selectedRange: $selectedRange, - disabledText: chat.chatInfo.userCantSendReason?.composeLabel + selectedRange: $selectedRange ) } else { SelectedItemsBottomToolbar( @@ -261,7 +260,9 @@ struct ChatView: View { groupLinkMemberRole: $groupLinkMemberRole, showTitle: true, creatingGroup: false, - isChannel: groupInfo.useRelays + isChannel: groupInfo.useRelays, + groupInfo: groupInfo, + composeState: $composeState ) } } @@ -512,6 +513,7 @@ struct ChatView: View { } ), scrollToItemId: $scrollToItemId, + composeState: $composeState, onSearch: { focusSearch() }, localAlias: groupInfo.localAlias ) @@ -743,7 +745,7 @@ struct ChatView: View { ChannelRelaysModel.shared.set(groupId: groupInfo.groupId, groupRelays: relays) } } - } else { + } else if groupInfo.membership.memberCurrent { Task { if let gInfo = await apiGetUpdatedGroupLinkData(groupInfo.groupId) { await MainActor.run { @@ -2109,7 +2111,7 @@ struct ChatView: View { func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View { let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading let live = composeState.liveMessage != nil - let canReply = ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live && !ci.localNote && selectedChatItems == nil + let canReply = ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live && !ci.localNote && selectedChatItems == nil && chat.chatInfo.sendMsgEnabled return ZStack(alignment: .trailing) { Image(systemName: "arrowshape.turn.up.left") .font(.system(size: 18)) @@ -2277,7 +2279,7 @@ struct ChatView: View { availableReactions.count > 0 { reactionsGroup } - if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live && !ci.localNote { + if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live && !ci.localNote && chat.chatInfo.sendMsgEnabled { replyButton } let fileSource = getLoadedFileSource(ci.file) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeChatLinkView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeChatLinkView.swift new file mode 100644 index 0000000000..650ea8a87f --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeChatLinkView.swift @@ -0,0 +1,42 @@ +import SwiftUI +import SimpleXChat + +struct ComposeChatLinkView: View { + @EnvironmentObject var theme: AppTheme + var chatLink: MsgChatLink + var cancelPreview: () -> Void + let cancelEnabled: Bool + + var body: some View { + HStack(alignment: .center, spacing: 8) { + ProfileImage( + imageStr: chatLink.image, + iconName: chatLink.iconName, + size: 44 + ) + .padding(.leading, 12) + VStack(alignment: .leading, spacing: 2) { + Text(chatLink.displayName) + .font(.headline) + .lineLimit(1) + if let descr = chatLink.shortDescription { + Text(descr) + .font(.caption) + .foregroundColor(theme.colors.secondary) + .lineLimit(1) + } + } + .padding(.vertical, 5) + Spacer() + if cancelEnabled { + Button { cancelPreview() } label: { + Image(systemName: "multiply") + } + } + } + .padding(.vertical, 8) + .padding(.trailing, 12) + .background(theme.appColors.sentMessage) + .frame(maxWidth: .infinity) + } +} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index f37eb614b9..5c57a46129 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -11,6 +11,7 @@ let MAX_NUMBER_OF_MENTIONS = 3 enum ComposePreview { case noPreview case linkPreview(linkPreview: LinkPreview?) + case chatLinkPreview(chatLink: MsgChatLink, ownerSig: LinkOwnerSig?) case mediaPreviews(mediaPreviews: [(String, UploadContent?)]) case voicePreview(recordingFileName: String, duration: Int) case filePreview(fileName: String, file: URL) @@ -73,7 +74,11 @@ struct ComposeState { } init(editingItem: ChatItem) { - let text = editingItem.content.text + let text = if case let .chat(t, chatLink, _) = editingItem.content.msgContent { + stripTextLink(t, chatLink.connLinkStr) + } else { + editingItem.content.text + } self.message = text self.parsedMessage = editingItem.formattedText ?? FormattedText.plain(text) self.preview = chatItemPreview(chatItem: editingItem) @@ -172,6 +177,7 @@ struct ComposeState { switch preview { case let .mediaPreviews(media): return !media.isEmpty case .voicePreview: return voiceMessageRecordingState == .finished + case .chatLinkPreview: return true case .filePreview: return true default: return !whitespaceOnly || forwarding || liveMessage != nil || submittingValidReport } @@ -183,6 +189,7 @@ struct ComposeState { var linkPreviewAllowed: Bool { switch preview { + case .chatLinkPreview: return false case .mediaPreviews: return false case .voicePreview: return false case .filePreview: return false @@ -238,6 +245,7 @@ struct ComposeState { switch preview { case .noPreview: false case .linkPreview: false + case .chatLinkPreview: false case let .mediaPreviews(mediaPreviews): !mediaPreviews.isEmpty case .voicePreview: false case .filePreview: true @@ -336,7 +344,6 @@ struct ComposeView: View { @Binding var keyboardVisible: Bool @Binding var keyboardHiddenDate: Date @Binding var selectedRange: NSRange - var disabledText: LocalizedStringKey? = nil @State var linkUrl: String? = nil @State var hasSimplexLink: Bool = false @@ -363,6 +370,8 @@ struct ComposeView: View { @UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false @AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = false + @AppStorage(GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS, store: groupDefaults) private var useLinkPreviews = true + @AppStorage(GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT, store: groupDefaults) private var linkPreviewsShowAlert = true @State private var updatingCompose = false @State private var relayListExpanded = false @StateObject private var channelRelaysModel = ChannelRelaysModel.shared @@ -382,34 +391,31 @@ struct ComposeView: View { Divider() } + let ownerState = ownerRelayState if let gInfo = chat.chatInfo.groupInfo, gInfo.useRelays, ![.memRejected, .memLeft, .memRemoved, .memGroupDeleted].contains(gInfo.membership.memberStatus) { if gInfo.membership.memberRole == .owner { - let relays = channelRelaysModel.groupId == gInfo.groupId - ? channelRelaysModel.groupRelays : [] - let failedCount = relays.filter { relayMemberConnFailed($0) != nil }.count - let activeCount = relays.filter { $0.relayStatus == .rsActive && relayMemberConnFailed($0) == nil }.count - if !relays.isEmpty && activeCount < relays.count { - ownerChannelRelayBar(relays: relays, activeCount: activeCount, failedCount: failedCount) + if let s = ownerState, s.relays.isEmpty || s.activeCount < s.relays.count { + ownerChannelRelayBar(relays: s.relays, activeCount: s.activeCount, failedCount: s.failedCount, removedCount: s.removedCount) } } else { let hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?? []).sorted() let relayMembers = chatModel.groupMembers - .filter { $0.wrapped.memberRole == .relay } + .filter { $0.wrapped.memberRole == .relay && ![.memRemoved, .memGroupDeleted].contains($0.wrapped.memberStatus) } .sorted { hostFromRelayLink($0.wrapped.relayLink ?? "") < hostFromRelayLink($1.wrapped.relayLink ?? "") } let showProgress = !gInfo.nextConnectPrepared || composeState.inProgress - let connectedCount = relayMembers.filter { $0.wrapped.activeConn?.connStatus == .ready }.count - let deletedCount = relayMembers.filter { $0.wrapped.activeConn?.connStatus == .deleted }.count - let failedCount = relayMembers.filter { $0.wrapped.activeConn?.connFailedErr != nil }.count - let errorCount = deletedCount + failedCount - let resolvedCount = connectedCount + deletedCount + let removedCount = relayMembers.filter { relayMemberRemoved($0.wrapped.memberStatus) }.count + let connectedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connStatus == .ready && $0.wrapped.activeConn?.connFailedErr == nil }.count + let failedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connFailedErr != nil }.count + let resolvedCount = connectedCount + removedCount + failedCount let total = relayMembers.count > 0 ? relayMembers.count : hostnames.count - if total > 0, !showProgress || resolvedCount < total { + if total == 0 || removedCount + failedCount > 0 || resolvedCount < total { subscriberChannelRelayBar( hostnames: hostnames, relayMembers: relayMembers, connectedCount: connectedCount, - errorCount: errorCount, + removedCount: removedCount, + failedCount: failedCount, total: total, showProgress: showProgress ) @@ -417,8 +423,9 @@ struct ComposeView: View { } } + let userCantSendReason = chat.chatInfo.userCantSendReason(allRelaysBroken: ownerState?.noActiveRelays ?? false) let composeEnabled = ( - chat.chatInfo.sendMsgEnabled || + userCantSendReason == nil || (chat.chatInfo.groupInfo?.nextConnectPrepared ?? false) || (chat.chatInfo.contact?.nextAcceptContactRequest ?? false) ) @@ -508,7 +515,7 @@ struct ComposeView: View { sendMessageView( disableSendButton, placeholder: chat.chatInfo.groupInfo.map { gi in - gi.useRelays && gi.membership.memberRole >= .owner + gi.useRelays && gi.membership.memberRole >= .owner && chat.chatInfo.groupChatScope() == nil ? NSLocalizedString("Broadcast", comment: "compose placeholder for channel owner") : nil } ?? nil @@ -521,7 +528,7 @@ struct ComposeView: View { .disabled(!composeEnabled) .if(!composeEnabled) { v in v.onTapGesture { - if let reason = chat.chatInfo.userCantSendReason { + if let reason = userCantSendReason { AlertManager.shared.showAlertMsg( title: "You can't send messages!", message: reason.alertMessage @@ -556,7 +563,7 @@ struct ComposeView: View { } else { composeState = composeState.copy(parsedMessage: parsedMsg ?? FormattedText.plain(msg)) } - if composeState.linkPreviewAllowed && UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) { + if composeState.linkPreviewAllowed && useLinkPreviews { if !msg.isEmpty { showLinkPreview(parsedMsg) } else { @@ -723,35 +730,94 @@ struct ComposeView: View { } } - private func ownerChannelRelayBar(relays: [GroupRelay], activeCount: Int, failedCount: Int) -> some View { + private var ownerRelayState: (relays: [GroupRelay], activeCount: Int, failedCount: Int, removedCount: Int, noActiveRelays: Bool)? { + guard let gInfo = chat.chatInfo.groupInfo, gInfo.useRelays, + gInfo.membership.memberRole == .owner, + ![.memLeft, .memRemoved, .memGroupDeleted].contains(gInfo.membership.memberStatus) + else { return nil } + guard channelRelaysModel.groupId == gInfo.groupId else { return nil } + let relays = channelRelaysModel.groupRelays + guard !relays.isEmpty else { return ([], 0, 0, 0, true) } + let relayMembers = relays.map { relay in + (relay, chatModel.groupMembers.first(where: { $0.wrapped.groupMemberId == relay.groupMemberId })?.wrapped) + } + let removedCount = relayMembers.filter { (_, m) in relayMemberRemoved(m?.memberStatus) }.count + let activeCount = relayMembers.filter { (relay, m) in !relayMemberRemoved(m?.memberStatus) && relay.relayStatus == .rsActive && m?.activeConn?.connFailedErr == nil }.count + let failedCount = relayMembers.filter { (_, m) in !relayMemberRemoved(m?.memberStatus) && m?.activeConn?.connFailedErr != nil }.count + let noActiveRelays = activeCount == 0 && (failedCount + removedCount) == relays.count + return (relays, activeCount, failedCount, removedCount, noActiveRelays) + } + + private var disabledText: LocalizedStringKey? { + chat.chatInfo.userCantSendReason(allRelaysBroken: ownerRelayState?.noActiveRelays ?? false)?.composeLabel + } + + @ViewBuilder private func ownerChannelRelayBar(relays: [GroupRelay], activeCount: Int, failedCount: Int, removedCount: Int) -> some View { let total = relays.count - let sorted = relays.sorted { relayDisplayName($0) < relayDisplayName($1) } - return VStack(spacing: 0) { + let allBroken = activeCount == 0 && (failedCount + removedCount) == total + let sorted = relays.map { relay in + (relay, chatModel.groupMembers.first(where: { $0.wrapped.groupMemberId == relay.groupMemberId })?.wrapped) + }.sorted { relayDisplayName($0.0) < relayDisplayName($1.0) } + VStack(spacing: 0) { relayBarHeader { - if activeCount + failedCount < total { + if !allBroken && activeCount + failedCount + removedCount < total { RelayProgressIndicator(active: activeCount, total: total) } - if failedCount > 0 { - Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays active, %d failed", comment: "channel relay bar progress with errors"), activeCount, total, failedCount)) + if total == 0 { + Text("No relays") + Image(systemName: "exclamationmark.triangle") + .foregroundColor(.orange) + } else if allBroken { + if removedCount == total { + Text("All relays removed") + } else if failedCount == total { + Text("All relays failed") + } else { + Text("No active relays") + } + Image(systemName: "exclamationmark.triangle") + .foregroundColor(.orange) + } else if activeCount + failedCount + removedCount >= total { + if failedCount > 0 && removedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d relays not active", comment: "channel relay bar"), failedCount + removedCount)) + } else if failedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d relays failed", comment: "channel relay bar"), failedCount)) + } else { + Text(String.localizedStringWithFormat(NSLocalizedString("%d relays removed", comment: "channel relay bar"), removedCount)) + } + } else if failedCount > 0 && removedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays active, %d errors", comment: "channel relay bar"), activeCount, total, failedCount + removedCount)) + } else if failedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays active, %d failed", comment: "channel relay bar"), activeCount, total, failedCount)) + } else if removedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays active, %d removed", comment: "channel relay bar"), activeCount, total, removedCount)) } else { Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays active", comment: "channel relay bar progress"), activeCount, total)) } } if relayListExpanded { - ForEach(sorted) { relay in - let failedErr = relayMemberConnFailed(relay) - if let err = failedErr { + if allBroken { + Text("Add relays to restore message delivery.") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption) + .foregroundColor(theme.colors.secondary) + .padding(.leading, 12) + .padding(.trailing) + .padding(.bottom, 4) + } + ForEach(sorted, id: \.0.id) { (relay, m) in + if let err = m?.activeConn?.connFailedErr { Button { showAlert( NSLocalizedString("Relay connection failed", comment: "alert title"), message: err ) } label: { - ownerRelayDetailRow(relay, connFailed: true) + ownerRelayDetailRow(relay, connFailed: true, memberStatus: m?.memberStatus) } .buttonStyle(.plain) } else { - ownerRelayDetailRow(relay, connFailed: false) + ownerRelayDetailRow(relay, connFailed: false, memberStatus: m?.memberStatus) } } } @@ -760,38 +826,74 @@ struct ComposeView: View { .animation(nil, value: relayListExpanded) } - private func ownerRelayDetailRow(_ relay: GroupRelay, connFailed: Bool) -> some View { + private func ownerRelayDetailRow(_ relay: GroupRelay, connFailed: Bool, memberStatus: GroupMemberStatus?) -> some View { relayBarDetailRow { Text(relayDisplayName(relay)).foregroundColor(theme.colors.secondary) Spacer() - relayStatusIndicator(relay.relayStatus, connFailed: connFailed) + relayStatusIndicator(relay.relayStatus, connFailed: connFailed, memberStatus: memberStatus) } } - private func subscriberChannelRelayBar( + @ViewBuilder private func subscriberChannelRelayBar( hostnames: [String], relayMembers: [GMember], connectedCount: Int, - errorCount: Int, + removedCount: Int, + failedCount: Int, total: Int, showProgress: Bool ) -> some View { + let errorCount = removedCount + failedCount + let allBroken = connectedCount == 0 && errorCount == total VStack(spacing: 0) { relayBarHeader { - if showProgress && connectedCount + errorCount < total { - RelayProgressIndicator(active: connectedCount, total: total) - } - if showProgress { - if errorCount > 0 { - Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays connected, %d errors", comment: "channel subscriber relay bar progress with errors"), connectedCount, total, errorCount)) + if total == 0 { + Text("No relays") + Image(systemName: "exclamationmark.triangle") + .foregroundColor(.orange) + } else if allBroken { + if removedCount == total { + Text("All relays removed") + } else if failedCount == total { + Text("All relays failed") + } else { + Text("No active relays") + } + Image(systemName: "exclamationmark.triangle") + .foregroundColor(.orange) + } else { + if showProgress && connectedCount + errorCount < total { + RelayProgressIndicator(active: connectedCount, total: total) + } + if connectedCount + removedCount + failedCount >= total, removedCount + failedCount > 0 { + if failedCount > 0 && removedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d relays not active", comment: "channel subscriber relay bar"), failedCount + removedCount)) + } else if failedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d relays failed", comment: "channel subscriber relay bar"), failedCount)) + } else if removedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d relays removed", comment: "channel subscriber relay bar"), removedCount)) + } + } else if failedCount > 0 && removedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays connected, %d errors", comment: "channel subscriber relay bar"), connectedCount, total, errorCount)) + } else if failedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays connected, %d failed", comment: "channel subscriber relay bar"), connectedCount, total, failedCount)) + } else if removedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays connected, %d removed", comment: "channel subscriber relay bar"), connectedCount, total, removedCount)) } else { Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays connected", comment: "channel subscriber relay bar progress"), connectedCount, total)) } - } else { - Text(String.localizedStringWithFormat(NSLocalizedString("%d relays", comment: "channel relay bar"), total)) } } if relayListExpanded { + if allBroken { + Text("Waiting for channel owner to add relays.") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption) + .foregroundColor(theme.colors.secondary) + .padding(.leading, 12) + .padding(.trailing) + .padding(.bottom, 4) + } if relayMembers.isEmpty { ForEach(hostnames, id: \.self) { relay in relayBarDetailRow { @@ -875,9 +977,9 @@ struct ComposeView: View { .padding(.vertical, 2) } - private func relayMemberConnFailed(_ relay: GroupRelay) -> String? { - chatModel.groupMembers.first(where: { $0.wrapped.groupMemberId == relay.groupMemberId })? - .wrapped.activeConn?.connFailedErr + + private func relayMemberRemoved(_ status: GroupMemberStatus?) -> Bool { + status.map { [.memLeft, .memRemoved, .memGroupDeleted].contains($0) } ?? false } private func connectButtonView(_ label: LocalizedStringKey, icon: String, connect: @escaping () -> Void) -> some View { @@ -920,7 +1022,7 @@ struct ComposeView: View { private func sendMessageView(_ disableSendButton: Bool, placeholder: String? = nil, sendToConnect: (() -> Void)? = nil) -> some View { ZStack(alignment: .leading) { SendMessageView( - placeholder: placeholder, + placeholder: disabledText != nil ? nil : placeholder, composeState: $composeState, selectedRange: $selectedRange, sendMessage: { ttl in @@ -1216,6 +1318,15 @@ struct ComposeView: View { cancelEnabled: !composeState.inProgress ) Divider() + case let .chatLinkPreview(chatLink, _): + ComposeChatLinkView( + chatLink: chatLink, + cancelPreview: { + composeState = composeState.copy(preview: .noPreview) + }, + cancelEnabled: !composeState.inProgress + ) + Divider() case let .mediaPreviews(mediaPreviews: media): ComposeImageView( images: media.map { (img, _) in img }, @@ -1374,6 +1485,10 @@ struct ComposeView: View { sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl, mentions: mentions) case .linkPreview: sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl, mentions: mentions) + case let .chatLinkPreview(chatLink, ownerSig): + let linkStr = chatLink.connLinkStr + let text = msgText.isEmpty ? linkStr : msgText + "\n" + linkStr + sent = await send(.chat(text: text, chatLink: chatLink, ownerSig: ownerSig), quoted: quoted, live: live, ttl: ttl, mentions: mentions) case let .mediaPreviews(media): // TODO: CHECK THIS let last = media.count - 1 @@ -1476,9 +1591,9 @@ struct ComposeView: View { return .file(msgText) case .report(_, let reason): return .report(text: msgText, reason: reason) - // TODO [short links] update chat link - case let .chat(_, chatLink): - return .chat(text: msgText, chatLink: chatLink) + case let .chat(_, chatLink, ownerSig): + let text = msgText.isEmpty ? chatLink.connLinkStr : msgText + "\n" + chatLink.connLinkStr + return .chat(text: text, chatLink: chatLink, ownerSig: ownerSig) case .unknown(let type, _): return .unknown(type: type, text: msgText) } @@ -1552,7 +1667,7 @@ struct ComposeView: View { type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, scope: chat.chatInfo.groupChatScope(), - sendAsGroup: chat.chatInfo.groupInfo.map { $0.useRelays && $0.membership.memberRole >= .owner } ?? false, + sendAsGroup: chat.chatInfo.sendAsGroup, live: live, ttl: ttl, composedMessages: msgs @@ -1773,21 +1888,55 @@ struct ComposeView: View { // Spec: spec/client/compose.md#loadLinkPreview private func loadLinkPreview(_ urlStr: String) { if pendingLinkUrl == urlStr, let url = URL(string: urlStr) { - composeState = composeState.copy(preview: .linkPreview(linkPreview: nil)) - getLinkPreview(url: url) { linkPreview in - if let linkPreview, pendingLinkUrl == urlStr { - privacyLinkPreviewsShowAlertGroupDefault.set(false) // to avoid showing alert to current users, show alert in v6.5 - composeState = composeState.copy(preview: .linkPreview(linkPreview: linkPreview)) - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - composeState = composeState.copy(preview: .noPreview) + if linkPreviewsShowAlert { + showLinkPreviewsConfirmAlert { enable in + if let enable { + linkPreviewsShowAlert = false + useLinkPreviews = enable + UserDefaults.standard.set(enable, forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) + if enable { + fetchLinkPreview(url, urlStr: urlStr) + } else { + pendingLinkUrl = nil + composeState = composeState.copy(preview: .noPreview) + } + } else { + cancelLinkPreview() } } - pendingLinkUrl = nil + return } + fetchLinkPreview(url, urlStr: urlStr) } } + private func fetchLinkPreview(_ url: URL, urlStr: String) { + composeState = composeState.copy(preview: .linkPreview(linkPreview: nil)) + getLinkPreview(url: url) { linkPreview in + if let linkPreview, pendingLinkUrl == urlStr { + composeState = composeState.copy(preview: .linkPreview(linkPreview: linkPreview)) + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + composeState = composeState.copy(preview: .noPreview) + } + } + pendingLinkUrl = nil + } + } + + private func showLinkPreviewsConfirmAlert(onChoice: @escaping (Bool?) -> Void) { + showAlert( + NSLocalizedString("Enable link previews?", comment: "alert title"), + message: NSLocalizedString("Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later.", comment: "alert message"), + actions: { + [ + UIAlertAction(title: NSLocalizedString("Disable", comment: "alert button"), style: .destructive) { _ in onChoice(false) }, + UIAlertAction(title: NSLocalizedString("Enable", comment: "alert button"), style: .default) { _ in onChoice(true) } + ] + } + ) + } + private func resetLinkPreview() { linkUrl = nil prevLinkUrl = nil diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index 845442c75f..1f328b2061 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -71,7 +71,7 @@ struct ContextItemView: View { } private func contextMsgPreview(_ contextItem: ChatItem) -> some View { - let r = messageText(contextItem.text, contextItem.formattedText, sender: nil, preview: true, mentions: contextItem.mentions, userMemberId: nil, showSecrets: nil, backgroundColor: UIColor(background)) + let r = messageText(contextItem.text, contextItem.formattedText, sender: nil, preview: true, mentions: contextItem.mentions, userMemberId: nil, showSecrets: nil, backgroundColor: UIColor(background), stripLink: contextItem.content.msgContent?.chatLinkStr) let t = attachment() + Text(AttributedString(r.string)) return t.if(r.hasSecrets, transform: hiddenSecretsView) @@ -83,6 +83,9 @@ struct ContextItemView: View { case .file: return isFileLoaded ? image("doc.fill") : Text("") case .image: return image("photo") case .voice: return isFileLoaded ? image("play.fill") : Text("") + case let .chat(_, chatLink, _): + let hasText = contextItem.text != chatLink.connLinkStr + return image(chatLink.smallIconName) + Text(chatLink.displayName) + Text(verbatim: hasText ? " - " : "") default: return Text("") } } diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupRelayView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupRelayView.swift new file mode 100644 index 0000000000..82b89beaa5 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupRelayView.swift @@ -0,0 +1,161 @@ +// +// AddGroupRelayView.swift +// SimpleX (iOS) +// +// Created by simplex on 29.04.2026. +// Copyright © 2026 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct AddGroupRelayView: View { + var groupInfo: GroupInfo + var existingRelayIds: Set + var onRelayAdded: () -> Void + @EnvironmentObject var theme: AppTheme + @Environment(\.dismiss) var dismiss + @State private var availableRelays: [(relayId: Int64, relay: UserChatRelay, operatorName: String?)] = [] + @State private var selectedRelayIds: Set = [] + @State private var isLoading = true + @State private var isAdding = false + + var body: some View { + NavigationView { + List { + if isLoading { + Section { + ProgressView() + .frame(maxWidth: .infinity) + } + } else if availableRelays.isEmpty { + Section { + Text("No available relays") + .foregroundColor(theme.colors.secondary) + } + } else { + Section { + ForEach(availableRelays, id: \.relayId) { item in + relayCheckRow(item.relayId, item.relay, operatorName: item.operatorName) + } + } + } + } + .modifier(ThemedBackground(grouped: true)) + .navigationTitle("Add relays") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("Add") { addSelectedRelays() } + .disabled(selectedRelayIds.isEmpty || isAdding) + } + } + } + .task { await loadAvailableRelays() } + } + + private func relayCheckRow(_ relayId: Int64, _ relay: UserChatRelay, operatorName: String?) -> some View { + let selected = selectedRelayIds.contains(relayId) + return Button { + if selected { + selectedRelayIds.remove(relayId) + } else { + selectedRelayIds.insert(relayId) + } + } label: { + HStack { + VStack(alignment: .leading) { + Text(chatRelayDisplayName(relay)) + .foregroundColor(theme.colors.onBackground) + .lineLimit(1) + if let opName = operatorName { + Text(opName) + .font(.caption) + .foregroundColor(theme.colors.secondary) + .lineLimit(1) + } + } + Spacer() + Image(systemName: selected ? "checkmark.circle.fill" : "circle") + .foregroundColor(selected ? theme.colors.primary : Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme)) + } + } + } + + private func loadAvailableRelays() async { + do { + let servers = try await getUserServers() + var relays: [(relayId: Int64, relay: UserChatRelay, operatorName: String?)] = [] + for op in servers { + if let oper = op.operator, oper.enabled != true { continue } + let opName: String? = op.operator?.operatorTag != nil ? op.operator?.tradeName : nil + for relay in op.chatRelays { + if relay.enabled && !relay.deleted, + let relayId = relay.chatRelayId, + !existingRelayIds.contains(relayId) { + relays.append((relayId, relay, opName)) + } + } + } + await MainActor.run { + availableRelays = relays + isLoading = false + } + } catch { + logger.error("loadAvailableRelays error: \(responseError(error))") + await MainActor.run { + isLoading = false + } + } + } + + private func addSelectedRelays() { + let relayIds = Array(selectedRelayIds) + guard !relayIds.isEmpty else { return } + isAdding = true + Task { + do { + guard let result = try await apiAddGroupRelays(groupInfo.groupId, relayIds: relayIds) else { + await MainActor.run { isAdding = false } + return + } + await MainActor.run { + isAdding = false + switch result { + case let .added(gInfo, _, relays): + ChannelRelaysModel.shared.set(groupId: gInfo.groupId, groupRelays: relays) + onRelayAdded() + dismiss() + case let .addFailed(results): + let successIds = Set(results.filter { $0.relayError == nil }.compactMap { $0.relay.chatRelayId }) + if !successIds.isEmpty { + selectedRelayIds.subtract(successIds) + availableRelays.removeAll { successIds.contains($0.relayId) } + onRelayAdded() + } + let errorLines = results.filter { $0.relayError != nil } + .map { "\(chatRelayDisplayName($0.relay)): \($0.relayError.map { connErrorText($0) } ?? "")" } + let successNames = results.filter { $0.relayError == nil } + .map { chatRelayDisplayName($0.relay) } + var msg = errorLines.joined(separator: "\n") + if !successNames.isEmpty { + msg += "\n" + String.localizedStringWithFormat(NSLocalizedString("Relays added: %@.", comment: "alert message"), successNames.joined(separator: ", ")) + } + showAlert( + NSLocalizedString("Error adding relays", comment: "alert title"), + message: msg + ) + } + } + } catch { + await MainActor.run { + isAdding = false + showAlert(NSLocalizedString("Error adding relays", comment: "alert title"), message: responseError(error)) + } + } + } + } +} diff --git a/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift b/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift index 1a4e384e24..6600cec47b 100644 --- a/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift +++ b/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift @@ -14,24 +14,49 @@ struct ChannelRelaysView: View { var groupInfo: GroupInfo @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme - @State private var groupRelays: [GroupRelay] = [] + @ObservedObject private var channelRelaysModel = ChannelRelaysModel.shared + @State private var showAddRelay = false + + private var groupRelays: [GroupRelay] { + channelRelaysModel.groupId == groupInfo.groupId ? channelRelaysModel.groupRelays : [] + } var body: some View { List { relaysList() + // TODO [relays] re-enable when relay management ships + // if groupInfo.isOwner { + // Section { + // Button { + // showAddRelay = true + // } label: { + // Label("Add relay", systemImage: "plus") + // } + // } + // } } + // TODO [relays] re-enable when relay management ships + // .sheet(isPresented: $showAddRelay) { + // let existingRelayIds = Set(groupRelays.filter { $0.relayStatus != .rsInactive }.compactMap { $0.userChatRelay.chatRelayId }) + // AddGroupRelayView(groupInfo: groupInfo, existingRelayIds: existingRelayIds) { + // Task { await chatModel.loadGroupMembers(groupInfo) } + // } + // } .onAppear { Task { await chatModel.loadGroupMembers(groupInfo) if groupInfo.isOwner { - groupRelays = await apiGetGroupRelays(groupInfo.groupId) + let relays = await apiGetGroupRelays(groupInfo.groupId) + await MainActor.run { + ChannelRelaysModel.shared.set(groupId: groupInfo.groupId, groupRelays: relays) + } } } } } @ViewBuilder private func relaysList() -> some View { - let relayMembers = chatModel.groupMembers.filter { $0.wrapped.memberRole == .relay } + let relayMembers = chatModel.groupMembers.filter { $0.wrapped.memberRole == .relay && $0.wrapped.memberStatus != .memRemoved && $0.wrapped.memberStatus != .memGroupDeleted } if relayMembers.isEmpty { Section { Text("No chat relays") @@ -40,7 +65,7 @@ struct ChannelRelaysView: View { } else { Section { ForEach(relayMembers) { member in - NavigationLink { + let link = NavigationLink { GroupMemberInfoView( groupInfo: groupInfo, chat: chat, @@ -55,6 +80,20 @@ struct ChannelRelaysView: View { : subscriberRelayStatusText(member.wrapped) relayMemberRow(member.wrapped, statusText: statusText) } + // TODO [relays] re-enable when relay management ships + // if groupInfo.isOwner && member.wrapped.canBeRemoved(groupInfo: groupInfo) { + // link.swipeActions(edge: .trailing) { + // Button { + // showRemoveMemberAlert(groupInfo, member.wrapped) + // } label: { + // Label("Remove relay", systemImage: "trash") + // } + // .tint(.red) + // } + // } else { + // link + // } + link } } footer: { Text("Chat relays forward messages to channel subscribers.") @@ -73,7 +112,9 @@ struct ChannelRelaysView: View { } private func ownerRelayStatusText(_ member: GroupMember) -> LocalizedStringKey { - if case .failed = member.activeConn?.connStatus { + if [.memLeft, .memRemoved, .memGroupDeleted].contains(member.memberStatus) { + relayConnStatus(member).text + } else if case .failed = member.activeConn?.connStatus { "failed" } else if member.activeConn?.connDisabled ?? false { "disabled" @@ -104,11 +145,16 @@ struct ChannelRelaysView: View { } func relayConnStatus(_ member: GroupMember) -> (text: LocalizedStringKey, color: Color) { - switch member.activeConn?.connStatus { - case .ready: ("connected", .green) - case .deleted: ("deleted", .red) - case .failed: ("failed", .red) - default: ("connecting", .yellow) + switch member.memberStatus { + case .memLeft: ("removed by operator", .red) + case .memRemoved, .memGroupDeleted: (member.memberStatus.text, .red) + default: + switch member.activeConn?.connStatus { + case .ready: ("connected", .green) + case .deleted: ("deleted", .red) + case .failed: ("failed", .red) + default: ("connecting", .yellow) + } } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index c02f4dae36..eee9500b3b 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -20,8 +20,10 @@ struct GroupChatInfoView: View { @ObservedObject var chat: Chat @Binding var groupInfo: GroupInfo @Binding var scrollToItemId: ChatItem.ID? + @Binding var composeState: ComposeState var onSearch: () -> Void @State var localAlias: String + @State private var showSharePicker = false @FocusState private var aliasTextFieldFocused: Bool @State private var alert: GroupChatInfoViewAlert? = nil @State private var groupLink: GroupLink? @@ -101,6 +103,10 @@ struct GroupChatInfoView: View { } } + let showUserSupportChat = groupInfo.membership.memberActive + && ((groupInfo.fullGroupPreferences.support.on && groupInfo.membership.memberRole < .moderator) + || groupInfo.membership.supportChat != nil) + if groupInfo.useRelays { Section { // TODO [relays] allow other owners to manage channel link (requires protocol changes to share link ownership) @@ -113,10 +119,21 @@ struct GroupChatInfoView: View { } label: { Label("Share link", systemImage: "square.and.arrow.up") } + Button { + showSharePicker = true + } label: { + Label("Share via chat", systemImage: "arrowshape.turn.up.forward") + } } if groupInfo.isOwner || members.contains(where: { $0.wrapped.memberRole >= .owner }) { channelMembersButton() } + if groupInfo.membership.memberRole >= .moderator { + memberSupportButton() + } + if showUserSupportChat { + UserSupportChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId) + } } footer: { if !groupInfo.isOwner && groupInfo.groupProfile.publicGroup?.groupLink != nil { Text("You can share a link or a QR code - anybody will be able to join the channel.") @@ -134,8 +151,7 @@ struct GroupChatInfoView: View { if groupInfo.canModerate { GroupReportsChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId) } - if groupInfo.membership.memberActive - && (groupInfo.membership.memberRole < .moderator || groupInfo.membership.supportChat != nil) { + if showUserSupportChat { UserSupportChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId) } } header: { @@ -150,19 +166,17 @@ struct GroupChatInfoView: View { if groupInfo.groupProfile.description != nil || (groupInfo.isOwner && groupInfo.businessChat == nil) { addOrEditWelcomeMessage() } - if !groupInfo.useRelays { - GroupPreferencesButton(groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, currentPreferences: groupInfo.fullGroupPreferences) - } + GroupPreferencesButton(groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, currentPreferences: groupInfo.fullGroupPreferences) } footer: { - if !groupInfo.useRelays { - let label: LocalizedStringKey = ( - groupInfo.businessChat == nil - ? "Only group owners can change group preferences." - : "Only chat owners can change preferences." - ) - Text(label) - .foregroundColor(theme.colors.secondary) - } + let label: LocalizedStringKey = ( + groupInfo.useRelays + ? "Only channel owners can change channel preferences." + : groupInfo.businessChat == nil + ? "Only group owners can change group preferences." + : "Only chat owners can change preferences." + ) + Text(label) + .foregroundColor(theme.colors.secondary) } Section { @@ -248,6 +262,9 @@ struct GroupChatInfoView: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .sheet(isPresented: $showSharePicker) { + shareChannelPicker(groupInfo: groupInfo, composeState: $composeState) + } .alert(item: $alert) { alertItem in switch(alertItem) { case .deleteGroupAlert: return deleteGroupAlert() @@ -638,7 +655,9 @@ struct GroupChatInfoView: View { groupLinkMemberRole: $groupLinkMemberRole, showTitle: false, creatingGroup: false, - isChannel: groupInfo.useRelays + isChannel: groupInfo.useRelays, + groupInfo: groupInfo, + composeState: $composeState ) .navigationBarTitle(groupInfo.useRelays ? "Channel link" : "Group link") .modifier(ThemedBackground(grouped: true)) @@ -905,26 +924,54 @@ struct GroupChatInfoView: View { } func showRemoveMemberAlert(_ groupInfo: GroupInfo, _ mem: GroupMember, dismiss: DismissAction? = nil) { - showAlert( - groupInfo.useRelays - ? NSLocalizedString("Remove subscriber?", comment: "alert title") - : NSLocalizedString("Remove member?", comment: "alert title"), - message: - groupInfo.useRelays - ? NSLocalizedString("Subscriber will be removed from channel - this cannot be undone!", comment: "alert message") - : groupInfo.businessChat == nil - ? NSLocalizedString("Member will be removed from group - this cannot be undone!", comment: "alert message") - : NSLocalizedString("Member will be removed from chat - this cannot be undone!", comment: "alert message"), - actions: {[ - UIAlertAction(title: NSLocalizedString("Remove", comment: "alert action"), style: .destructive) { _ in - removeMember(groupInfo, mem, withMessages: false, dismiss: dismiss) - }, - UIAlertAction(title: NSLocalizedString("Remove and delete messages", comment: "alert action"), style: .destructive) { _ in - removeMember(groupInfo, mem, withMessages: true, dismiss: dismiss) - }, - cancelAlertAction - ]} - ) + if mem.memberRole == .relay { + let isLastActive = groupInfo.useRelays && mem.memberCurrent && { + let activeRelays = ChatModel.shared.groupMembers.filter { $0.wrapped.memberRole == .relay && $0.wrapped.memberCurrent } + return activeRelays.count <= 1 + }() + showAlert( + NSLocalizedString("Remove relay?", comment: "alert title"), + message: isLastActive + ? NSLocalizedString("This is the last active relay. Removing it will prevent message delivery to subscribers.", comment: "alert message") + : NSLocalizedString("Relay will be removed from channel - this cannot be undone!", comment: "alert message"), + actions: {[ + UIAlertAction(title: NSLocalizedString("Remove", comment: "alert action"), style: .destructive) { _ in + removeMember(groupInfo, mem, withMessages: false, dismiss: dismiss) + }, + cancelAlertAction + ]} + ) + } else if groupInfo.useRelays { + showAlert( + NSLocalizedString("Remove subscriber?", comment: "alert title"), + message: NSLocalizedString("Subscriber will be removed from channel - this cannot be undone!", comment: "alert message"), + actions: {[ + UIAlertAction(title: NSLocalizedString("Remove", comment: "alert action"), style: .destructive) { _ in + removeMember(groupInfo, mem, withMessages: false, dismiss: dismiss) + }, + UIAlertAction(title: NSLocalizedString("Remove and delete messages", comment: "alert action"), style: .destructive) { _ in + removeMember(groupInfo, mem, withMessages: true, dismiss: dismiss) + }, + cancelAlertAction + ]} + ) + } else { + showAlert( + NSLocalizedString("Remove member?", comment: "alert title"), + message: groupInfo.businessChat == nil + ? NSLocalizedString("Member will be removed from group - this cannot be undone!", comment: "alert message") + : NSLocalizedString("Member will be removed from chat - this cannot be undone!", comment: "alert message"), + actions: {[ + UIAlertAction(title: NSLocalizedString("Remove", comment: "alert action"), style: .destructive) { _ in + removeMember(groupInfo, mem, withMessages: false, dismiss: dismiss) + }, + UIAlertAction(title: NSLocalizedString("Remove and delete messages", comment: "alert action"), style: .destructive) { _ in + removeMember(groupInfo, mem, withMessages: true, dismiss: dismiss) + }, + cancelAlertAction + ]} + ) + } } func removeMember(_ groupInfo: GroupInfo, _ mem: GroupMember, withMessages: Bool, dismiss: DismissAction?) { @@ -976,7 +1023,9 @@ struct GroupPreferencesButton: View { var creatingGroup: Bool = false private var label: LocalizedStringKey { - groupInfo.businessChat == nil ? "Group preferences" : "Chat preferences" + groupInfo.useRelays ? "Channel preferences" + : groupInfo.businessChat == nil ? "Group preferences" + : "Chat preferences" } var body: some View { @@ -993,7 +1042,9 @@ struct GroupPreferencesButton: View { .navigationBarTitleDisplayMode(.large) .onDisappear { let saveText = NSLocalizedString( - creatingGroup ? "Save" : "Save and notify group members", + creatingGroup ? "Save" + : groupInfo.useRelays ? "Save and notify subscribers" + : "Save and notify group members", comment: "alert button" ) @@ -1048,12 +1099,66 @@ func largeGroupReceiptsDisabledAlert() -> Alert { ) } +@ViewBuilder +func shareChannelPicker(groupInfo: GroupInfo, composeState: Binding? = nil) -> some View { + let v = ChatItemForwardingView( + title: "Share channel", + isProhibited: { $0.prohibitedByPref(hasSimplexLink: true, isMediaOrFileAttachment: false, isVoice: false) }, + onSelectChat: { chat in shareChatLink(chat, sourceGroupInfo: groupInfo, composeState: composeState) } + ) + if #available(iOS 16.0, *) { + v.presentationDetents([.fraction(0.8)]) + } else { + v + } +} + +func shareChatLink(_ destChat: Chat, sourceGroupInfo: GroupInfo, composeState: Binding? = nil) { + let sendAsGroup = if let gInfo = destChat.chatInfo.groupInfo { gInfo.useRelays && gInfo.membership.memberRole >= .owner } else { false } + Task { + do { + let mc = try await apiShareChatMsgContent( + shareChatType: .group, shareChatId: Int64(sourceGroupInfo.groupId), + toChatType: destChat.chatInfo.chatType, toChatId: destChat.chatInfo.apiId, + toScope: destChat.chatInfo.groupChatScope(), sendAsGroup: sendAsGroup + ) + if case let .chat(_, chatLink, ownerSig) = mc { + await MainActor.run { + dismissAllSheets { + let cs = ComposeState(preview: .chatLinkPreview(chatLink: chatLink, ownerSig: ownerSig)) + if let composeState { + composeState.wrappedValue = cs + } else { + ChatModel.shared.draft = cs + ChatModel.shared.draftChatId = destChat.id + } + if destChat.id != ChatModel.shared.chatId { + ItemsModel.shared.loadOpenChat(destChat.id) + } + } + } + } else { + logger.error("shareChatLink: unexpected MsgContent: \(String(describing: mc))") + await MainActor.run { + showAlert(NSLocalizedString("Error sharing channel", comment: "alert title"), message: String(describing: mc)) + } + } + } catch { + logger.error("shareChatLink error: \(error.localizedDescription)") + await MainActor.run { + showAlert(NSLocalizedString("Error sharing channel", comment: "alert title"), message: error.localizedDescription) + } + } + } +} + struct GroupChatInfoView_Previews: PreviewProvider { static var previews: some View { GroupChatInfoView( chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: Binding.constant(GroupInfo.sampleData), scrollToItemId: Binding.constant(nil), + composeState: Binding.constant(ComposeState()), onSearch: {}, localAlias: "" ) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index 56ee370402..22253c4808 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -18,7 +18,10 @@ struct GroupLinkView: View { var showTitle: Bool = false var creatingGroup: Bool = false var isChannel: Bool = false + var groupInfo: GroupInfo? = nil + var composeState: Binding? = nil var linkCreatedCb: (() -> Void)? = nil + @State private var showSharePicker = false @State private var showShortLink = true @State private var creatingLink = false @State private var alert: GroupLinkAlert? @@ -104,6 +107,11 @@ struct GroupLinkView: View { } label: { Label("Share link", systemImage: "square.and.arrow.up") } + if groupInfo?.groupProfile.publicGroup != nil { + Button { showSharePicker = true } label: { + Label("Share via chat", systemImage: "arrowshape.turn.up.forward") + } + } if !creatingGroup && !isChannel { Button(role: .destructive) { alert = .deleteLink } label: { @@ -160,6 +168,11 @@ struct GroupLinkView: View { } } .modifier(ThemedBackground(grouped: true)) + .sheet(isPresented: $showSharePicker) { + if let gInfo = groupInfo { + shareChannelPicker(groupInfo: gInfo, composeState: composeState) + } + } } private func createGroupLink() { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index af7054db01..883a768d97 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -121,13 +121,15 @@ struct GroupMemberInfoView: View { } if connectionLoaded { + let showMemberSupportChat = !openedFromSupportChat + && groupInfo.membership.memberRole >= .moderator + && member.memberRole != .relay + && ((groupInfo.fullGroupPreferences.support.on && member.memberRole < .moderator) + || member.supportChat != nil) if member.memberActive { Section { - if !openedFromSupportChat - && groupInfo.membership.memberRole >= .moderator - && member.memberRole != .relay - && (member.memberRole < .moderator || member.supportChat != nil) { + if showMemberSupportChat { MemberInfoSupportChatNavLink(groupInfo: groupInfo, member: groupMember, scrollToItemId: $scrollToItemId) } if let code = connectionCode, @@ -142,6 +144,10 @@ struct GroupMemberInfoView: View { // synchronizeConnectionButtonForce() // } } + } else if groupInfo.useRelays && member.memberCurrent && showMemberSupportChat { + Section { + MemberInfoSupportChatNavLink(groupInfo: groupInfo, member: groupMember, scrollToItemId: $scrollToItemId) + } } if let contactLink = member.contactLink { @@ -635,13 +641,12 @@ struct GroupMemberInfoView: View { blockForAllButton(mem) } } - // TODO [relays] removing relay should also remove its link from group link data; - // TODO - removing last relay should be prohibited or show warning + // TODO [relays] re-enable when relay management ships if canRemove && mem.memberRole != .relay { - if mem.memberStatus == .memRemoved || mem.memberStatus == .memLeft { - deleteMemberMessagesButton(mem) - } else { + if mem.memberStatus != .memRemoved && (mem.memberStatus != .memLeft || mem.memberRole == .relay) { removeMemberButton(mem) + } else if mem.memberRole != .relay { + deleteMemberMessagesButton(mem) } } } @@ -699,7 +704,10 @@ struct GroupMemberInfoView: View { Button(role: .destructive) { showRemoveMemberAlert(groupInfo, mem, dismiss: dismiss) } label: { - Label(groupInfo.useRelays ? "Remove subscriber" : "Remove member", systemImage: "trash") + let text = mem.memberRole == .relay ? "Remove relay" + : groupInfo.useRelays ? "Remove subscriber" + : "Remove member" + Label(text, systemImage: "trash") .foregroundColor(.red) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index 55b1dc6d2e..cc2feef706 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -27,26 +27,50 @@ struct GroupPreferencesView: View { @State private var showSaveDialogue = false var body: some View { - let saveText: LocalizedStringKey = creatingGroup ? "Save" : "Save and notify group members" + let saveText: LocalizedStringKey = creatingGroup ? "Save" : groupInfo.useRelays ? "Save and notify subscribers" : "Save and notify group members" VStack { List { - Section { - MemberAdmissionButton( - groupInfo: $groupInfo, - admission: groupInfo.groupProfile.memberAdmission_, - currentAdmission: groupInfo.groupProfile.memberAdmission_, - creatingGroup: creatingGroup - ) + if !groupInfo.useRelays { + Section { + MemberAdmissionButton( + groupInfo: $groupInfo, + admission: groupInfo.groupProfile.memberAdmission_, + currentAdmission: groupInfo.groupProfile.memberAdmission_, + creatingGroup: creatingGroup + ) + } + featureSection(.timedMessages, $preferences.timedMessages.enable) + featureSection(.fullDelete, $preferences.fullDelete.enable) + featureSection(.directMessages, $preferences.directMessages.enable, $preferences.directMessages.role) + featureSection(.reactions, $preferences.reactions.enable) + featureSection(.voice, $preferences.voice.enable, $preferences.voice.role) + featureSection(.files, $preferences.files.enable, $preferences.files.role) + featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role) + featureSection(.reports, $preferences.reports.enable, disabled: true) // enable reports in 7.0 once directory support added + featureSection(.history, $preferences.history.enable) + featureSection(.support, $preferences.support.enable, disabled: true) + } else { + featureSection(.timedMessages, $preferences.timedMessages.enable) + featureSection(.fullDelete, $preferences.fullDelete.enable) + featureSection(.reactions, $preferences.reactions.enable) + featureSection(.history, $preferences.history.enable) + let supportNotice = NSLocalizedString("Chats with admins in public channels have no E2E encryption - use only with trusted chat relays.", comment: "alert message") + featureSection(.support, $preferences.support.enable, notice: supportNotice) + .onChange(of: preferences.support.enable) { enable in + if enable == .on { + showAlert( + NSLocalizedString("Enable chats with admins?", comment: "alert title"), + message: supportNotice, + actions: {[ + UIAlertAction(title: NSLocalizedString("Enable", comment: "alert button"), style: .destructive) { _ in }, + UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel) { _ in + preferences.support.enable = .off + } + ]} + ) + } + } } - featureSection(.timedMessages, $preferences.timedMessages.enable) - featureSection(.fullDelete, $preferences.fullDelete.enable) - featureSection(.directMessages, $preferences.directMessages.enable, $preferences.directMessages.role) - featureSection(.reactions, $preferences.reactions.enable) - featureSection(.voice, $preferences.voice.enable, $preferences.voice.role) - featureSection(.files, $preferences.files.enable, $preferences.files.role) - featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role) - featureSection(.reports, $preferences.reports.enable) - featureSection(.history, $preferences.history.enable) if groupInfo.isOwner { Section { @@ -85,7 +109,7 @@ struct GroupPreferencesView: View { } } - private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding, _ enableForRole: Binding? = nil) -> some View { + private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding, _ enableForRole: Binding? = nil, disabled: Bool = false, notice: String? = nil) -> some View { Section { let color: Color = enableFeature.wrappedValue == .on ? .green : theme.colors.secondary let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon @@ -96,9 +120,9 @@ struct GroupPreferencesView: View { set: { on, _ in enableFeature.wrappedValue = on ? .on : .off } ) settingsRow(icon, color: color) { - Toggle(feature.text, isOn: enable) + Toggle(feature.text(isChannel: groupInfo.isChannel), isOn: enable) } - .disabled(feature == .reports) // remove in 6.4 + .disabled(disabled) if timedOn { DropdownCustomTimePicker( selection: $preferences.timedMessages.ttl, @@ -119,7 +143,7 @@ struct GroupPreferencesView: View { } } else { settingsRow(icon, color: color) { - infoRow(Text(feature.text), enableFeature.wrappedValue.text) + infoRow(Text(feature.text(isChannel: groupInfo.isChannel)), enableFeature.wrappedValue.text) } if timedOn { infoRow("Delete after", timeText(preferences.timedMessages.ttl)) @@ -137,8 +161,11 @@ struct GroupPreferencesView: View { } } } footer: { - Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.isOwner)) - .foregroundColor(theme.colors.secondary) + VStack(alignment: .leading) { + Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.isOwner, isChannel: groupInfo.isChannel)) + if let notice { Text(notice) } + } + .foregroundColor(theme.colors.secondary) } .onChange(of: enableFeature.wrappedValue) { enabled in if case .off = enabled { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 69587c0152..126fcc57b3 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -37,12 +37,12 @@ struct GroupProfileView: View { var body: some View { List { - EditProfileImage(profileImage: $groupProfile.image, showChooseSource: $showChooseSource) + EditProfileImage(profileImage: $groupProfile.image, iconName: groupInfo.chatIconName, showChooseSource: $showChooseSource) .if(!focusDisplayName) { $0.padding(.top) } Section { HStack { - TextField("Group display name", text: $groupProfile.displayName) + TextField(groupInfo.useRelays ? "Channel display name" : "Group display name", text: $groupProfile.displayName) .focused($focusDisplayName) if !validNewProfileName { Button { @@ -54,7 +54,7 @@ struct GroupProfileView: View { } let fullName = groupInfo.groupProfile.fullName if fullName != "" && fullName != groupProfile.displayName { - TextField("Group full name (optional)", text: $groupProfile.fullName) + TextField(groupInfo.useRelays ? "Channel full name (optional)" : "Group full name (optional)", text: $groupProfile.fullName) } HStack { TextField("Short description", text: $shortDescr) @@ -67,7 +67,7 @@ struct GroupProfileView: View { } } } footer: { - Text("Group profile is stored on members' devices, not on the servers.") + Text(groupInfo.useRelays ? "Channel profile is stored on subscribers' devices and on the chat relays." : "Group profile is stored on members' devices, not on the servers.") } Section { @@ -80,11 +80,11 @@ struct GroupProfileView: View { currentProfileHash == groupProfile.hashValue && (groupInfo.groupProfile.shortDescr ?? "") == shortDescr.trimmingCharacters(in: .whitespaces) ) - Button("Save group profile", action: saveProfile) + Button(groupInfo.useRelays ? "Save channel profile" : "Save group profile", action: saveProfile) .disabled(!canUpdateProfile) } } - .confirmationDialog("Group image", isPresented: $showChooseSource, titleVisibility: .visible) { + .confirmationDialog(groupInfo.useRelays ? "Channel image" : "Group image", isPresented: $showChooseSource, titleVisibility: .visible) { Button("Take picture") { showTakePhoto = true } @@ -130,9 +130,15 @@ struct GroupProfileView: View { .onDisappear { if canUpdateProfile { showAlert( - title: NSLocalizedString("Save group profile?", comment: "alert title"), - message: NSLocalizedString("Group profile was changed. If you save it, the updated profile will be sent to group members.", comment: "alert message"), - buttonTitle: NSLocalizedString("Save (and notify members)", comment: "alert button"), + title: groupInfo.useRelays + ? NSLocalizedString("Save channel profile?", comment: "alert title") + : NSLocalizedString("Save group profile?", comment: "alert title"), + message: groupInfo.useRelays + ? NSLocalizedString("Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers.", comment: "alert message") + : NSLocalizedString("Group profile was changed. If you save it, the updated profile will be sent to group members.", comment: "alert message"), + buttonTitle: groupInfo.useRelays + ? NSLocalizedString("Save (and notify subscribers)", comment: "alert button") + : NSLocalizedString("Save (and notify members)", comment: "alert button"), buttonAction: saveProfile, cancelButton: true ) @@ -142,14 +148,14 @@ struct GroupProfileView: View { switch a { case let .saveError(err): return Alert( - title: Text("Error saving group profile"), + title: Text(groupInfo.useRelays ? "Error saving channel profile" : "Error saving group profile"), message: Text(err) ) case let .invalidName(name): return createInvalidNameAlert(name, $groupProfile.displayName) } } - .navigationBarTitle("Group profile") + .navigationBarTitle(groupInfo.useRelays ? "Channel profile" : "Group profile") .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(focusDisplayName ? .inline : .large) } diff --git a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift index 3dc27c08f6..880933985c 100644 --- a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift +++ b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift @@ -45,7 +45,7 @@ struct MemberSupportView: View { : membersWithChats.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) } if membersWithChats.isEmpty { - Text("No chats with members") + Text(groupInfo.fullGroupPreferences.support.on ? "No chats with members" : "Chats with members are disabled") .foregroundColor(.secondary) } else { List { diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 3050b0d4cd..dc4971aafa 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -38,8 +38,9 @@ enum PresetTag: Int, Identifiable, CaseIterable, Equatable { case favorites = 1 case contacts = 2 case groups = 3 - case business = 4 - case notes = 5 + case channels = 4 + case business = 5 + case notes = 6 var id: Int { rawValue } @@ -293,36 +294,40 @@ struct ChatListView: View { @ToolbarContentBuilder var topToolbar: some ToolbarContent { ToolbarItem(placement: .topBarLeading) { leadingToolbarItem } - ToolbarItem(placement: .principal) { SubsStatusIndicator() } + ToolbarItem(placement: .principal) { if !shouldShowOnboarding { SubsStatusIndicator() } } ToolbarItem(placement: .topBarTrailing) { trailingToolbarItem } } - + @ToolbarContentBuilder var bottomToolbar: some ToolbarContent { let padding: Double = Self.hasHomeIndicator ? 0 : 14 ToolbarItem(placement: .bottomBar) { HStack { leadingToolbarItem.padding(.bottom, padding) Spacer() - SubsStatusIndicator().padding(.bottom, padding) - Spacer() + if !shouldShowOnboarding { + SubsStatusIndicator().padding(.bottom, padding) + Spacer() + } trailingToolbarItem.padding(.bottom, padding) } .contentShape(Rectangle()) .onTapGesture { scrollToSearchBar = true } } } - + @ToolbarContentBuilder func bottomToolbarGroup() -> some ToolbarContent { let padding: Double = Self.hasHomeIndicator ? 0 : 14 ToolbarItemGroup(placement: viewOnScreen ? .bottomBar : .principal) { leadingToolbarItem.padding(.bottom, padding) Spacer() - SubsStatusIndicator().padding(.bottom, padding) - Spacer() + if !shouldShowOnboarding { + SubsStatusIndicator().padding(.bottom, padding) + Spacer() + } trailingToolbarItem.padding(.bottom, padding) } } - + @ViewBuilder var leadingToolbarItem: some View { let user = chatModel.currentUser ?? User.sampleData ZStack(alignment: .topTrailing) { @@ -348,7 +353,34 @@ struct ChatListView: View { } } - private var chatList: some View { + private var shouldShowOnboarding: Bool { + !addressCreationCardShown && !chatModel.chats.isEmpty && !hasConversations + } + + private var hasConversations: Bool { + chatModel.chats.contains { chat in + switch chat.chatInfo { + case .local: return false + case let .direct(contact): return !contact.chatDeleted && !contact.isContactCard + case .group: return true + case .contactRequest: return false + case .contactConnection: return false + case .invalidJSON: return false + } + } + } + + @ViewBuilder private var chatList: some View { + if shouldShowOnboarding { + ConnectOnboardingView() + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .modifier(ThemedBackground()) + } else { + chatListContent + } + } + + private var chatListContent: some View { let cs = filteredChats() return ZStack { ScrollViewReader { scrollProxy in @@ -369,6 +401,13 @@ struct ChatListView: View { .padding(.top, oneHandUI ? 8 : 0) .id("searchBar") } + if !oneHandUICardShown { + OneHandUICard() + .padding(.vertical, 6) + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + } if #available(iOS 16.0, *) { ForEach(cs, id: \.viewId) { chat in ChatListNavLink(chat: chat, parentSheet: $sheet) @@ -388,15 +427,8 @@ struct ChatListView: View { .disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id)) } } - if !oneHandUICardShown { - OneHandUICard() - .padding(.vertical, 6) - .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) - .listRowSeparator(.hidden) - .listRowBackground(Color.clear) - } - if !addressCreationCardShown { - AddressCreationCard() + if !addressCreationCardShown && hasConversations { + ConnectBannerCard() .padding(.vertical, 6) .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) .listRowSeparator(.hidden) @@ -807,11 +839,11 @@ struct TagsView: View { nil } let active = tag == selectedPresetTag - let (icon, text) = presetTagLabel(tag: tag, active: active) + let (icon, menuIcon, text) = presetTagLabel(tag: tag, active: active) let color: Color = active ? .accentColor : .secondary HStack(spacing: 4) { - Image(systemName: icon) + Image(systemName: menuIcon ?? icon) .foregroundColor(color) ZStack { Text(text).fontWeight(.semibold).foregroundColor(.clear) @@ -854,9 +886,9 @@ struct TagsView: View { Button { setActiveFilter(filter: .presetTag(tag)) } label: { - let (systemName, text) = presetTagLabel(tag: tag, active: tag == selectedPresetTag) + let (icon, _, text) = presetTagLabel(tag: tag, active: tag == selectedPresetTag) HStack { - Image(systemName: systemName) + Image(systemName: icon) Text(text) } } @@ -864,8 +896,8 @@ struct TagsView: View { } } label: { if let tag = selectedPresetTag, tag.сollapse { - let (systemName, _) = presetTagLabel(tag: tag, active: true) - Image(systemName: systemName) + let (icon, menuIcon, _) = presetTagLabel(tag: tag, active: true) + Image(systemName: menuIcon ?? icon) .foregroundColor(.accentColor) } else { Image(systemName: "list.bullet") @@ -875,14 +907,15 @@ struct TagsView: View { .frame(minWidth: 28) } - private func presetTagLabel(tag: PresetTag, active: Bool) -> (String, LocalizedStringKey) { + private func presetTagLabel(tag: PresetTag, active: Bool) -> (item: String, menu: String?, label: LocalizedStringKey) { switch tag { - case .groupReports: (active ? "flag.fill" : "flag", "Reports") - case .favorites: (active ? "star.fill" : "star", "Favorites") - case .contacts: (active ? "person.fill" : "person", "Contacts") - case .groups: (active ? "person.2.fill" : "person.2", "Groups") - case .business: (active ? "briefcase.fill" : "briefcase", "Businesses") - case .notes: (active ? "folder.fill" : "folder", "Notes") + case .groupReports: (item: active ? "flag.fill" : "flag", menu: nil, label: "Reports") + case .favorites: (item: active ? "star.fill" : "star", menu: nil, label: "Favorites") + case .contacts: (item: active ? "person.fill" : "person", menu: nil, label: "Contacts") + case .groups: (item: active ? "person.2.fill" : "person.2", menu: nil, label: "Groups") + case .channels: (item: active ? "antenna.radiowaves.left.and.right.circle.fill" : "antenna.radiowaves.left.and.right", menu: "antenna.radiowaves.left.and.right", label: "Channels") + case .business: (item: active ? "briefcase.fill" : "briefcase", menu: nil, label: "Businesses") + case .notes: (item: active ? "folder.fill" : "folder", menu: nil, label: "Notes") } } @@ -924,7 +957,12 @@ func presetTagMatchesChat(_ tag: PresetTag, _ chatInfo: ChatInfo, _ chatStats: C } case .groups: switch chatInfo { - case let .group(groupInfo, _): groupInfo.businessChat == nil + case let .group(groupInfo, _): groupInfo.businessChat == nil && !groupInfo.isChannel + default: false + } + case .channels: + switch chatInfo { + case let .group(groupInfo, _): groupInfo.isChannel default: false } case .business: diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 112e4099c0..243d804685 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -296,11 +296,23 @@ struct ChatPreviewView: View { } func chatItemPreview(_ cItem: ChatItem) -> (Text, Bool) { - let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText() - let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil + let (itemText, itemFormattedText) = chatItemPreviewText(cItem) let r = messageText(itemText, itemFormattedText, sender: cItem.meta.showGroupAsSender ? nil : cItem.memberDisplayName, preview: true, mentions: cItem.mentions, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, showSecrets: nil, backgroundColor: UIColor(theme.colors.background), prefix: prefix()) return (Text(AttributedString(r.string)), r.hasSecrets) + func chatItemPreviewText(_ ci: ChatItem) -> (String, [FormattedText]?) { + if ci.meta.itemDeleted != nil { + return (markedDeletedText(), nil) + } + if case let .chat(_, chatLink, _) = ci.content.msgContent { + let descr = if let descr = chatLink.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines), + descr != "" { "\n" + descr } else { "" } + let text = chatLink.displayName + descr + return (text, nil) + } + return (ci.text(isChannel: chat.chatInfo.isChannel), ci.formattedText) + } + // same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey; // can be refactored into a single function if functions calling these are changed to return same type func markedDeletedText() -> String { @@ -426,6 +438,18 @@ struct ChatPreviewView: View { smallContentPreviewFile(size: dynamicMediaSize) { CIFileView(file: ci.file, edited: ci.meta.itemEdited, smallViewSize: dynamicMediaSize) } + case let .chat(_, chatLink, ownerSig): + smallContentPreview(size: dynamicMediaSize, borderColor: chatLink.image != nil ? .secondary : .clear) { + ProfileImage( + imageStr: chatLink.image, + iconName: chatLink.iconName, + size: dynamicMediaSize, + color: Color(uiColor: .tertiaryLabel) + ) + .onTapGesture { + planAndConnect(chatLink.connLinkStr, linkOwnerSig: ownerSig, theme: theme, dismiss: false) + } + } default: EmptyView() } } @@ -499,12 +523,12 @@ func flagIcon(size: CGFloat, color: Color) -> some View { .foregroundColor(color) } -func smallContentPreview(size: CGFloat, _ view: @escaping () -> some View) -> some View { +func smallContentPreview(size: CGFloat, borderColor: Color = .secondary, _ view: @escaping () -> some View) -> some View { view() .frame(width: size, height: size) .cornerRadius(8) .overlay(RoundedRectangle(cornerSize: CGSize(width: 8, height: 8)) - .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)) + .strokeBorder(borderColor, lineWidth: 0.3, antialiased: true)) .padding(.vertical, size / 6) .padding(.leading, 3) .offset(x: 6) diff --git a/apps/ios/Shared/Views/ChatList/OneHandUICard.swift b/apps/ios/Shared/Views/ChatList/OneHandUICard.swift index 059f24cc82..132a19d7e7 100644 --- a/apps/ios/Shared/Views/ChatList/OneHandUICard.swift +++ b/apps/ios/Shared/Views/ChatList/OneHandUICard.swift @@ -11,27 +11,46 @@ import SimpleXChat struct OneHandUICard: View { @EnvironmentObject var theme: AppTheme - @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true @AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false + @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial @State private var showOneHandUIAlert = false var body: some View { - ZStack(alignment: .topTrailing) { - VStack(alignment: .leading, spacing: 8) { - Text("Toggle chat list:").font(.title3) - Toggle("Reachable chat toolbar", isOn: $oneHandUI) + HStack(spacing: 2) { + segment( + icon: "platter.filled.bottom.and.arrow.down.iphone", + text: "Bottom bar", + isSelected: oneHandUI + ) { + withAnimation { oneHandUI = true } } - Image(systemName: "multiply") - .foregroundColor(theme.colors.secondary) - .onTapGesture { - showOneHandUIAlert = true + .background { if oneHandUI { Color(uiColor: .systemGray5) } } + .background(ToolbarMaterial.material(toolbarMaterial)) + ZStack(alignment: .trailing) { + segment( + icon: "platter.filled.top.and.arrow.up.iphone", + text: "Top bar", + isSelected: !oneHandUI + ) { + withAnimation { oneHandUI = false } } + Image(systemName: "multiply") + .foregroundColor(theme.colors.secondary) + .frame(width: 12, height: 12) + .padding(.vertical, 4) + .padding(.trailing, 16) + .padding(.leading, 4) + .contentShape(Rectangle()) + .onTapGesture { + showOneHandUIAlert = true + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background { if !oneHandUI { Color(uiColor: .systemGray5) } } + .background(ToolbarMaterial.material(toolbarMaterial)) } - .padding() - .background(theme.appColors.sentMessage) - .cornerRadius(12) - .frame(height: dynamicSize(userFont).rowHeight) + .clipShape(Capsule()) .alert(isPresented: $showOneHandUIAlert) { Alert( title: Text("Reachable chat toolbar"), @@ -44,6 +63,22 @@ struct OneHandUICard: View { ) } } + + private func segment(icon: String, text: LocalizedStringKey, isSelected: Bool, action: @escaping () -> Void) -> some View { + HStack(spacing: 8) { + Image(systemName: icon) + .font(.body) + .foregroundColor(isSelected ? theme.colors.secondary : theme.colors.primary) + Text(text) + .font(.subheadline) + .foregroundColor(isSelected ? theme.colors.secondary : theme.colors.onBackground) + } + .padding(.leading, 16) + .padding(.vertical, 4) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + .contentShape(Rectangle()) + .onTapGesture { action() } + } } #Preview { diff --git a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift index 76bdc898d5..56e343588d 100644 --- a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift +++ b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift @@ -110,8 +110,8 @@ struct MigrateToAppGroupView: View { do { resetChatCtrl() try initializeChat(start: true) - onboardingStageDefault.set(.step4_SetNotificationsMode) - chatModel.onboardingStage = .step4_SetNotificationsMode + onboardingStageDefault.set(.step4_NetworkCommitments) + chatModel.onboardingStage = .step4_NetworkCommitments setV3DBMigration(.ready) } catch let error { dbContainerGroupDefault.set(.documents) diff --git a/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift index 980308f13c..0491b38575 100644 --- a/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift +++ b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift @@ -37,6 +37,7 @@ struct ChatItemClipped: ViewModifier { .rcvMsgContent, .rcvDecryptionError, .rcvIntegrityError, + .rcvMsgError, .invalidJSON: let tail = if let mc = ci.content.msgContent, mc.isImageOrVideo && mc.text.isEmpty { false diff --git a/apps/ios/Shared/Views/Helpers/DetermineWidth.swift b/apps/ios/Shared/Views/Helpers/DetermineWidth.swift index b05ab17089..54e9fe0e80 100644 --- a/apps/ios/Shared/Views/Helpers/DetermineWidth.swift +++ b/apps/ios/Shared/Views/Helpers/DetermineWidth.swift @@ -21,6 +21,19 @@ struct DetermineWidth: View { } } +struct DetermineHeight: View { + typealias Key = MaximumHeightPreferenceKey + var body: some View { + GeometryReader { proxy in + Color.clear + .preference( + key: MaximumHeightPreferenceKey.self, + value: proxy.size.height + ) + } + } +} + struct DetermineWidthImageVideoItem: View { typealias Key = MaximumWidthImageVideoPreferenceKey var body: some View { @@ -41,6 +54,13 @@ struct MaximumWidthPreferenceKey: PreferenceKey { } } +struct MaximumHeightPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = max(value, nextValue()) + } +} + struct MaximumWidthImageVideoPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index 2ef928c7c5..9f2fc833ba 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -86,6 +86,40 @@ func showSheet( } } +func openExternalLink(_ url: URL) { + let s = url.absoluteString + if s.starts(with: "https://simplex.chat/contact#") || (s.starts(with: "https://smp") && s.contains(".simplex.im/a#")) { + ChatModel.shared.appOpenUrl = url + } else { + showAlert( + title: NSLocalizedString("Open external link?", comment: "alert title"), + message: s, + buttonTitle: NSLocalizedString("Open", comment: "alert button"), + buttonAction: { UIApplication.shared.open(url) }, + cancelButton: true + ) + } +} + +struct ExternalLink: View { + let destination: URL + let label: Label + + init(destination: URL, @ViewBuilder label: () -> Label) { + self.destination = destination + self.label = label() + } + + init(_ titleKey: LocalizedStringKey, destination: URL) where Label == Text { + self.destination = destination + self.label = Text(titleKey) + } + + var body: some View { + Button { openExternalLink(destination) } label: { label } + } +} + let okAlertAction = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default) let cancelAlertAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel) @@ -101,25 +135,28 @@ class OpenChatAlertViewController: UIViewController { private let profileFullName: String private let profileImage: UIView private let subtitle: String? + private let information: String? private let cancelTitle: String - private let confirmTitle: String + private let confirmTitle: String? private let onCancel: () -> Void - private let onConfirm: () -> Void + private let onConfirm: (() -> Void)? init( profileName: String, profileFullName: String, profileImage: UIView, subtitle: String? = nil, + information: String? = nil, cancelTitle: String = "Cancel", - confirmTitle: String = "Open", - onCancel: @escaping () -> Void, - onConfirm: @escaping () -> Void + confirmTitle: String? = "Open", + onCancel: @escaping () -> Void = {}, + onConfirm: (() -> Void)? = nil ) { self.profileName = profileName self.profileFullName = profileFullName self.profileImage = profileImage self.subtitle = subtitle + self.information = information self.cancelTitle = cancelTitle self.confirmTitle = confirmTitle self.onCancel = onCancel @@ -180,12 +217,24 @@ class OpenChatAlertViewController: UIViewController { subtitleLabel.text = subtitle subtitleLabel.font = UIFont.preferredFont(forTextStyle: .footnote) subtitleLabel.textColor = .secondaryLabel - subtitleLabel.numberOfLines = 1 + subtitleLabel.numberOfLines = 3 subtitleLabel.textAlignment = .center subtitleLabel.translatesAutoresizingMaskIntoConstraints = false profileViews.append(subtitleLabel) } + // Information label (e.g. owner verification) + if let information { + let infoLabel = UILabel() + infoLabel.text = information + infoLabel.font = UIFont.preferredFont(forTextStyle: .footnote) + infoLabel.textColor = .label + infoLabel.numberOfLines = 3 + infoLabel.textAlignment = .center + infoLabel.translatesAutoresizingMaskIntoConstraints = false + profileViews.append(infoLabel) + } + // Horizontal stack for image + name let stack = UIStackView(arrangedSubviews: profileViews) stack.axis = .vertical @@ -211,20 +260,54 @@ class OpenChatAlertViewController: UIViewController { cancelButton.titleLabel?.font = UIFont(descriptor: bodyDescr.withSymbolicTraits(.traitBold) ?? bodyDescr, size: 0) cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside) - let confirmButton = UIButton(type: .system) - confirmButton.setTitle(confirmTitle, for: .normal) - confirmButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) - confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside) + let buttonStack: UIStackView + var buttonDividerConstraints: [NSLayoutConstraint] = [] - let verticalButtons = cancelButton.intrinsicContentSize.width + 20 >= alertWidth / 2 || confirmButton.intrinsicContentSize.width + 20 >= alertWidth / 2 + if let confirmTitle { + let confirmButton = UIButton(type: .system) + confirmButton.setTitle(confirmTitle, for: .normal) + confirmButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside) - // Button stack with equal width buttons - let buttonStack = UIStackView(arrangedSubviews: verticalButtons ? [confirmButton, cancelButton] : [cancelButton, confirmButton]) - buttonStack.axis = verticalButtons ? .vertical : .horizontal - buttonStack.distribution = .fillEqually - buttonStack.spacing = 0 // no spacing, use divider instead - buttonStack.translatesAutoresizingMaskIntoConstraints = false - buttonStack.heightAnchor.constraint(greaterThanOrEqualToConstant: alertButtonHeight * (verticalButtons ? 2 : 1)).isActive = true + let verticalButtons = cancelButton.intrinsicContentSize.width + 20 >= alertWidth / 2 || confirmButton.intrinsicContentSize.width + 20 >= alertWidth / 2 + + // Button stack with equal width buttons + buttonStack = UIStackView(arrangedSubviews: verticalButtons ? [confirmButton, cancelButton] : [cancelButton, confirmButton]) + buttonStack.axis = verticalButtons ? .vertical : .horizontal + buttonStack.distribution = .fillEqually + buttonStack.spacing = 0 // no spacing, use divider instead + buttonStack.translatesAutoresizingMaskIntoConstraints = false + buttonStack.heightAnchor.constraint(greaterThanOrEqualToConstant: alertButtonHeight * (verticalButtons ? 2 : 1)).isActive = true + + // Add divider between buttons + let buttonDivider = UIView() + buttonDivider.backgroundColor = UIColor.separator + buttonDivider.translatesAutoresizingMaskIntoConstraints = false + buttonStack.addSubview(buttonDivider) + + buttonDividerConstraints = if verticalButtons { + [ + buttonDivider.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + buttonDivider.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + buttonDivider.centerYAnchor.constraint(equalTo: buttonStack.centerYAnchor), + buttonDivider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale) + ] + } else { + [ + buttonDivider.topAnchor.constraint(equalTo: buttonStack.topAnchor), + buttonDivider.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + buttonDivider.centerXAnchor.constraint(equalTo: buttonStack.centerXAnchor), + buttonDivider.widthAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale) + ] + } + } else { + // Single button + buttonStack = UIStackView(arrangedSubviews: [cancelButton]) + buttonStack.axis = .horizontal + buttonStack.distribution = .fillEqually + buttonStack.translatesAutoresizingMaskIntoConstraints = false + buttonStack.heightAnchor.constraint(greaterThanOrEqualToConstant: alertButtonHeight).isActive = true + } // Vertical stack containing hStack and buttonStack let vStack = UIStackView(arrangedSubviews: [topRowContainer, buttonStack]) @@ -241,29 +324,6 @@ class OpenChatAlertViewController: UIViewController { horizontalDivider.translatesAutoresizingMaskIntoConstraints = false containerView.addSubview(horizontalDivider) - // Add divider between buttons - let buttonDivider = UIView() - buttonDivider.backgroundColor = UIColor.separator - buttonDivider.translatesAutoresizingMaskIntoConstraints = false - buttonStack.addSubview(buttonDivider) - - // Constraints - let buttonDividerConstraints = if verticalButtons { - [ - buttonDivider.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - buttonDivider.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - buttonDivider.centerYAnchor.constraint(equalTo: buttonStack.centerYAnchor), - buttonDivider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale) - ] - } else { - [ - buttonDivider.topAnchor.constraint(equalTo: buttonStack.topAnchor), - buttonDivider.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - buttonDivider.centerXAnchor.constraint(equalTo: buttonStack.centerXAnchor), - buttonDivider.widthAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale) - ] - } - NSLayoutConstraint.activate([ // Container view centering and fixed width containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), @@ -295,7 +355,7 @@ class OpenChatAlertViewController: UIViewController { @objc private func confirmTapped() { dismiss(animated: true) { - self.onConfirm() + self.onConfirm?() } } } @@ -307,10 +367,11 @@ func showOpenChatAlert( profileImage: Content, theme: AppTheme, subtitle: String? = nil, + information: String? = nil, cancelTitle: String = "Cancel", - confirmTitle: String = "Open", + confirmTitle: String? = "Open", onCancel: @escaping () -> Void = {}, - onConfirm: @escaping () -> Void + onConfirm: (() -> Void)? = nil ) { let themedView = profileImage.environmentObject(theme) let hostingController = UIHostingController(rootView: themedView) @@ -323,6 +384,7 @@ func showOpenChatAlert( profileFullName: profileFullName, profileImage: hostedView, subtitle: subtitle, + information: information, cancelTitle: cancelTitle, confirmTitle: confirmTitle, onCancel: onCancel, diff --git a/apps/ios/Shared/Views/NewChat/AddChannelView.swift b/apps/ios/Shared/Views/NewChat/AddChannelView.swift index 098cccef1b..32d6e7fe2c 100644 --- a/apps/ios/Shared/Views/NewChat/AddChannelView.swift +++ b/apps/ios/Shared/Views/NewChat/AddChannelView.swift @@ -10,6 +10,7 @@ import SwiftUI import SimpleXChat struct AddChannelView: View { + @Environment(\.colorScheme) var colorScheme @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme @StateObject private var channelRelaysModel = ChannelRelaysModel.shared @@ -45,28 +46,39 @@ struct AddChannelView: View { private func profileStepView() -> some View { List { Group { - ZStack(alignment: .center) { - ZStack(alignment: .topTrailing) { - ProfileImage(imageStr: profile.image, size: 128) - if profile.image != nil { - Button { - profile.image = nil - } label: { - Image(systemName: "multiply") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 12) + HStack(spacing: 0) { + Spacer(minLength: 0) + ZStack(alignment: .center) { + ZStack(alignment: .topTrailing) { + ProfileImage(imageStr: profile.image, iconName: "antenna.radiowaves.left.and.right.circle.fill", size: 128) + if profile.image != nil { + Button { + profile.image = nil + } label: { + Image(systemName: "multiply") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 12) + } } } + editImageButton { showChooseSource = true } + .buttonStyle(BorderlessButtonStyle()) } - editImageButton { showChooseSource = true } - .buttonStyle(BorderlessButtonStyle()) + .padding(.horizontal, 10) // Offsets transparent space built into 3D asset + #if SIMPLEX_ASSETS + Spacer(minLength: 0) + Image(colorScheme == .light ? "create-channel" : "create-channel-light") + .resizable() + .scaledToFit() + .frame(height: 140) + #endif + Spacer(minLength: 0) } - .frame(maxWidth: .infinity, alignment: .center) } .listRowBackground(Color.clear) .listRowSeparator(.hidden) - .listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0)) + .listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0)) Section { channelNameTextField() @@ -161,7 +173,10 @@ struct AddChannelView: View { private func createChannel() { focusDisplayName = false profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces) - profile.groupPreferences = GroupPreferences(history: GroupPreference(enable: .on)) + profile.groupPreferences = GroupPreferences( + history: GroupPreference(enable: .on), + support: GroupPreference(enable: .off) + ) creationInProgress = true Task { do { @@ -174,20 +189,32 @@ struct AddChannelView: View { } return } - guard let (gInfo, gLink, gRelays) = try await apiNewPublicGroup( + guard let result = try await apiNewPublicGroup( incognito: false, relayIds: relayIds, groupProfile: profile ) else { await MainActor.run { creationInProgress = false } return } - await MainActor.run { - m.updateGroup(gInfo) - m.creatingChannelId = gInfo.id - groupInfo = gInfo - groupLink = gLink - groupRelays = gRelays.sorted { relayDisplayName($0) < relayDisplayName($1) } - channelRelaysModel.set(groupId: gInfo.groupId, groupRelays: gRelays) - creationInProgress = false + switch result { + case let .created(gInfo, gLink, gRelays): + await MainActor.run { + m.updateGroup(gInfo) + m.creatingChannelId = gInfo.id + groupInfo = gInfo + groupLink = gLink + groupRelays = gRelays.sorted { relayDisplayName($0) < relayDisplayName($1) } + channelRelaysModel.set(groupId: gInfo.groupId, groupRelays: gRelays) + creationInProgress = false + } + case let .creationFailed(relayResults): + await MainActor.run { + creationInProgress = false + showAlert( + NSLocalizedString("Error creating channel", comment: "alert title"), + message: NSLocalizedString("Relay results:", comment: "alert message") + "\n" + + relayResults.map { "\(chatRelayDisplayName($0.relay)): \($0.relayError.map { connErrorText($0) } ?? "ok")" }.joined(separator: "\n") + ) + } } } catch { await MainActor.run { @@ -210,6 +237,7 @@ struct AddChannelView: View { var operatorGroups: [[UserChatRelay]] = [] var customRelays: [UserChatRelay] = [] for op in servers { + guard op.operator?.enabled ?? true else { continue } let relays = op.chatRelays.filter { $0.enabled && !$0.deleted && $0.chatRelayId != nil } guard !relays.isEmpty else { continue } if op.operator != nil { @@ -244,6 +272,7 @@ struct AddChannelView: View { private func checkHasRelays() async -> Bool { guard let servers = try? await getUserServers() else { return false } return servers.contains { op in + (op.operator?.enabled ?? true) && op.chatRelays.contains { $0.enabled && !$0.deleted && $0.chatRelayId != nil } } } @@ -256,7 +285,7 @@ struct AddChannelView: View { let total = groupRelays.count return List { Group { - ProfileImage(imageStr: gInfo.groupProfile.image, size: 128) + ProfileImage(imageStr: gInfo.groupProfile.image, iconName: "antenna.radiowaves.left.and.right.circle.fill", size: 128) .frame(maxWidth: .infinity, alignment: .center) Text(gInfo.groupProfile.displayName) @@ -309,24 +338,27 @@ struct AddChannelView: View { .compactSectionSpacing() Section { - Button("Channel link") { + Button("Cancel and delete channel", role: .destructive) { + showCancelChannelAlert(gInfo) + } + Button("Continue") { if activeCount >= total { showLinkStep = true } else if activeCount > 0 { let actions: [UIAlertAction] = if activeCount + failedCount < total { [ - UIAlertAction(title: NSLocalizedString("Proceed", comment: "alert action"), style: .default) { _ in showLinkStep = true }, + UIAlertAction(title: NSLocalizedString("Continue", comment: "alert action"), style: .default) { _ in showLinkStep = true }, UIAlertAction(title: NSLocalizedString("Wait", comment: "alert action"), style: .cancel) { _ in } ] } else { [ - UIAlertAction(title: NSLocalizedString("Proceed", comment: "alert action"), style: .default) { _ in showLinkStep = true }, + UIAlertAction(title: NSLocalizedString("Continue", comment: "alert action"), style: .default) { _ in showLinkStep = true }, cancelAlertAction ] } showAlert( NSLocalizedString("Not all relays connected", comment: "alert title"), - message: String.localizedStringWithFormat(NSLocalizedString("Channel will start working with %d of %d relays. Proceed?", comment: "alert message"), activeCount, total), + message: String.localizedStringWithFormat(NSLocalizedString("Channel will start working with %d of %d relays. Continue?", comment: "alert message"), activeCount, total), actions: { actions } ) } @@ -336,9 +368,9 @@ struct AddChannelView: View { } .navigationTitle("Creating channel") .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Cancel") { cancelChannelCreation(gInfo) } + .onDisappear { + if !showLinkStep && m.creatingChannelId == gInfo.id { + showCancelChannelAlert(gInfo) } } .onChange(of: channelRelaysModel.groupRelays) { relays in @@ -373,7 +405,8 @@ struct AddChannelView: View { groupLinkMemberRole: Binding.constant(.observer), // TODO [relays] starting role should be communicated in protocol from owner to relays showTitle: false, creatingGroup: true, - isChannel: true + isChannel: true, + groupInfo: gInfo ) { m.creatingChannelId = nil DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -399,6 +432,24 @@ struct AddChannelView: View { } } + private func showCancelChannelAlert(_ gInfo: GroupInfo) { + let activeCount = groupRelays.filter { $0.relayStatus == .rsActive && relayMemberConnFailed($0) == nil }.count + let total = groupRelays.count + showAlert( + NSLocalizedString("Cancel creating channel?", comment: "alert title"), + message: String.localizedStringWithFormat( + NSLocalizedString("Your new channel %@ is connected to %d of %d relays.\nIf you cancel, the channel will be deleted - you can create it again.", comment: "alert message"), + gInfo.groupProfile.displayName, activeCount, total + ), + actions: {[ + UIAlertAction(title: NSLocalizedString("Wait", comment: "alert action"), style: .cancel) { _ in }, + UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert action"), style: .destructive) { _ in + cancelChannelCreation(gInfo) + } + ]} + ) + } + // MARK: - Helpers private func showInvalidChannelNameAlert() { @@ -428,9 +479,15 @@ func relayDisplayName(_ relay: GroupRelay) -> String { return "relay \(relay.groupRelayId)" } -func relayStatusIndicator(_ status: RelayStatus, connFailed: Bool = false) -> some View { - let color: Color = connFailed ? .red : (status == .rsActive ? .green : .yellow) - let text: LocalizedStringKey = connFailed ? "failed" : status.text +func chatRelayDisplayName(_ relay: UserChatRelay) -> String { + if !relay.displayName.isEmpty { return relay.displayName } + return relay.address +} + +func relayStatusIndicator(_ status: RelayStatus, connFailed: Bool = false, memberStatus: GroupMemberStatus? = nil) -> some View { + let removed = memberStatus.map { [.memLeft, .memRemoved, .memGroupDeleted].contains($0) } ?? false + let color: Color = connFailed || removed ? .red : (status == .rsActive ? .green : .yellow) + let text: LocalizedStringKey = connFailed ? "failed" : memberStatus == .memLeft ? "removed by operator" : removed ? "removed" : status.text return HStack(spacing: 4) { Circle() .fill(color) diff --git a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift index 3a64a955c5..6add190b88 100644 --- a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift +++ b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift @@ -26,7 +26,7 @@ struct AddContactLearnMore: View { VStack(alignment: .leading, spacing: 18) { Text("To connect, your contact can scan QR code or use the link in the app.") Text("If you can't meet in person, show QR code in a video call, or share the link.") - Text("Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).") + ExternalLink("Read more in User Guide.", destination: URL(string: "https://simplex.chat/docs/guide/readme.html#connect-to-friends")!) } .frame(maxWidth: .infinity, alignment: .leading) .listRowBackground(Color.clear) diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index c74e016974..47afee5f06 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -10,6 +10,7 @@ import SwiftUI import SimpleXChat struct AddGroupView: View { + @Environment(\.colorScheme) var colorScheme @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @@ -66,29 +67,40 @@ struct AddGroupView: View { func createGroupView() -> some View { List { Group { - ZStack(alignment: .center) { - ZStack(alignment: .topTrailing) { - ProfileImage(imageStr: profile.image, size: 128) - if profile.image != nil { - Button { - profile.image = nil - } label: { - Image(systemName: "multiply") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 12) + HStack(spacing: 0) { + Spacer(minLength: 0) + ZStack(alignment: .center) { + ZStack(alignment: .topTrailing) { + ProfileImage(imageStr: profile.image, iconName: "person.2.circle.fill", size: 128) + if profile.image != nil { + Button { + profile.image = nil + } label: { + Image(systemName: "multiply") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 12) + } } } - } - editImageButton { showChooseSource = true } - .buttonStyle(BorderlessButtonStyle()) // otherwise whole "list row" is clickable + editImageButton { showChooseSource = true } + .buttonStyle(BorderlessButtonStyle()) // otherwise whole "list row" is clickable + } + .padding(.horizontal, 10) // Offsets transparent space built into 3D asset + #if SIMPLEX_ASSETS + Spacer(minLength: 0) + Image(colorScheme == .light ? "create-group" : "create-group-light") + .resizable() + .scaledToFit() + .frame(height: 140) + #endif + Spacer(minLength: 0) } - .frame(maxWidth: .infinity, alignment: .center) } .listRowBackground(Color.clear) .listRowSeparator(.hidden) - .listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0)) + .listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0)) Section { groupNameTextField() diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index 8e62923f3f..177f8761f4 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -55,7 +55,7 @@ struct NewChatSheet: View { let showArchive = chatModel.chats.contains { $0.chatInfo.contact?.chatDeleted == true } let v = NavigationView { viewBody(showArchive) - .navigationTitle("New message") + .navigationTitle("New chat") .navigationBarTitleDisplayMode(.large) .navigationBarHidden(searchMode) .modifier(ThemedBackground(grouped: true)) @@ -99,9 +99,8 @@ struct NewChatSheet: View { Section { NavigationLink(isActive: $isAddContactActive) { NewChatView(selection: .invite) - .navigationTitle("New chat") .modifier(ThemedBackground(grouped: true)) - .navigationBarTitleDisplayMode(.large) + .navigationBarTitleDisplayMode(.inline) } label: { navigateOnTap(Label("Create 1-time link", systemImage: "link.badge.plus")) { isAddContactActive = true @@ -109,9 +108,8 @@ struct NewChatSheet: View { } NavigationLink(isActive: $isScanPasteLinkActive) { NewChatView(selection: .connect, showQRCodeScanner: true) - .navigationTitle("New chat") .modifier(ThemedBackground(grouped: true)) - .navigationBarTitleDisplayMode(.large) + .navigationBarTitleDisplayMode(.inline) } label: { navigateOnTap(Label("Scan / Paste link", systemImage: "qrcode")) { isScanPasteLinkActive = true @@ -131,7 +129,7 @@ struct NewChatSheet: View { .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { - Label("Create public channel (BETA)", systemImage: "antenna.radiowaves.left.and.right.circle.fill") + Label("Create public channel (BETA)", systemImage: "antenna.radiowaves.left.and.right") } } diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 63fb7f5221..9bcc326a66 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -80,6 +80,7 @@ struct NewChatView: View { @EnvironmentObject var theme: AppTheme @State var selection: NewChatOption @State var showQRCodeScanner = false + var onboarding: Bool = false @State private var invitationUsed: Bool = false @State private var connLinkInvitation: CreatedConnLink = CreatedConnLink(connFullLink: "", connShortLink: nil) @State private var showShortLink = true @@ -91,17 +92,19 @@ struct NewChatView: View { var body: some View { VStack(alignment: .leading) { - Picker("New chat", selection: $selection) { - Label("1-time link", systemImage: "link") - .tag(NewChatOption.invite) - Label("Connect via link", systemImage: "qrcode") - .tag(NewChatOption.connect) - } - .pickerStyle(.segmented) - .padding() - .onChange(of: $selection.wrappedValue) { opt in - if opt == NewChatOption.connect { - showQRCodeScanner = true + if !onboarding { + Picker("New chat", selection: $selection) { + Label("1-time link", systemImage: "link") + .tag(NewChatOption.invite) + Label("Connect via link", systemImage: "qrcode") + .tag(NewChatOption.connect) + } + .pickerStyle(.segmented) + .padding() + .onChange(of: $selection.wrappedValue) { opt in + if opt == NewChatOption.connect { + showQRCodeScanner = true + } } } @@ -116,7 +119,7 @@ struct NewChatView: View { } } if case .connect = selection { - ConnectView(showQRCodeScanner: $showQRCodeScanner, pastedLink: $pastedLink, alert: $alert) + ConnectView(showQRCodeScanner: $showQRCodeScanner, pastedLink: $pastedLink, alert: $alert, onboarding: onboarding) .transition(.move(edge: .trailing)) } } @@ -141,16 +144,22 @@ struct NewChatView: View { } default: () } - } + }, + including: onboarding ? .subviews : .all ) } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - InfoSheetButton { - AddContactLearnMore(showTitle: true) + if !onboarding { + InfoSheetButton { + AddContactLearnMore(showTitle: true) + } + } else { + Image(systemName: "info.circle").opacity(0) } } } + .if(onboarding) { $0.navigationBarTitleDisplayMode(.inline) } .modifier(ThemedBackground(grouped: true)) .onChange(of: invitationUsed) { used in if used && !(m.showingInvitation?.connChatUsed ?? true) { @@ -179,7 +188,8 @@ struct NewChatView: View { contactConnection: $contactConnection, connLinkInvitation: $connLinkInvitation, showShortLink: $showShortLink, - choosingProfile: $choosingProfile + choosingProfile: $choosingProfile, + onboarding: onboarding ) } else if creatingConnReq { creatingLinkProgressView() @@ -239,6 +249,7 @@ private func incognitoProfileImage() -> some View { } private struct InviteView: View { + @Environment(\.colorScheme) var colorScheme @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme @Binding var invitationUsed: Bool @@ -246,18 +257,19 @@ private struct InviteView: View { @Binding var connLinkInvitation: CreatedConnLink @Binding var showShortLink: Bool @Binding var choosingProfile: Bool + var onboarding: Bool = false @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false var body: some View { List { - Section(header: Text("Share this 1-time invite link").foregroundColor(theme.colors.secondary)) { + Section(header: sectionHeader) { shareLinkView() } .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10)) qrCodeView() - if let selectedProfile = chatModel.currentUser { + if !onboarding, let selectedProfile = chatModel.currentUser { Section { NavigationLink { ActiveProfilePicker( @@ -281,9 +293,9 @@ private struct InviteView: View { } header: { Text("Share profile").foregroundColor(theme.colors.secondary) } footer: { - if incognitoDefault { - Text("A new random profile will be shared.") - } + if incognitoDefault { + Text("A new random profile will be shared.") + } } } } @@ -295,8 +307,35 @@ private struct InviteView: View { } } + private var sectionHeader: some View { + #if SIMPLEX_ASSETS + VStack(alignment: .leading, spacing: 0) { + Image(colorScheme == .light + ? (onboarding ? "one-time-link" : "one-time-link-small") + : (onboarding ? "one-time-link-light" : "one-time-link-small-light")) + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) + sectionHeaderText + } + .padding(.bottom, 6) + #else + sectionHeaderText + .if(onboarding) { $0.padding(.bottom, 6) } + #endif + } + + @ViewBuilder private var sectionHeaderText: some View { + if onboarding { + Text("Send the link via any messenger - it's secure. Ask to paste into SimpleX.") + .font(.body).foregroundColor(theme.colors.onBackground).textCase(nil) + } else { + Text("Share this 1-time invite link").foregroundColor(theme.colors.secondary) + } + } + private func shareLinkView() -> some View { - HStack { + HStack(spacing: 8) { let link = connLinkInvitation.simplexChatUri(short: showShortLink) linkTextView(link) Button { @@ -305,6 +344,7 @@ private struct InviteView: View { } label: { Image(systemName: "square.and.arrow.up") .padding(.top, -7) + .padding(.horizontal, 8) } } .frame(maxWidth: .infinity) @@ -324,7 +364,11 @@ private struct InviteView: View { .listRowSeparator(.hidden) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } header: { - ToggleShortLinkHeader(text: Text("Or show this code"), link: connLinkInvitation, short: $showShortLink) + if onboarding { + Text("Or show QR in person or via video call.").font(.body).foregroundColor(theme.colors.onBackground).textCase(nil) + } else { + ToggleShortLinkHeader(text: Text("Or show this code"), link: connLinkInvitation, short: $showShortLink) + } } } @@ -587,20 +631,24 @@ private struct ActiveProfilePicker: View { } private struct ConnectView: View { + @Environment(\.colorScheme) var colorScheme @StateObject private var connectProgressManager = ConnectProgressManager.shared @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var theme: AppTheme @Binding var showQRCodeScanner: Bool @Binding var pastedLink: String @Binding var alert: NewChatViewAlert? + var onboarding: Bool = false @State var scannerPaused: Bool = false @State private var pasteboardHasStrings = UIPasteboard.general.hasStrings var body: some View { List { - Section(header: Text("Paste the link you received").foregroundColor(theme.colors.secondary)) { + Section(header: connectSectionHeader) { pasteLinkView() } + .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)) + Section(header: Text("Or scan QR code").foregroundColor(theme.colors.secondary)) { ScannerInView(showQRCodeScanner: $showQRCodeScanner, scannerPaused: $scannerPaused, processQRCode: processQRCode) } @@ -630,7 +678,7 @@ private struct ConnectView: View { } } } label: { - Text("Tap to paste link") + Text("Tap to paste link").foregroundColor(theme.colors.primary) } .disabled(!pasteboardHasStrings) .frame(maxWidth: .infinity, alignment: .center) @@ -669,6 +717,23 @@ private struct ConnectView: View { } } + private var connectSectionHeader: some View { + #if SIMPLEX_ASSETS + VStack(alignment: .leading, spacing: 0) { + Image(colorScheme == .light + ? (onboarding ? "connect-via-link" : "connect-via-link-small") + : (onboarding ? "connect-via-link-light" : "connect-via-link-small-light")) + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) + Text("Paste the link you received").foregroundColor(theme.colors.secondary) + } + .padding(.bottom, 4) + #else + Text("Paste the link you received").foregroundColor(theme.colors.secondary) + #endif + } + private func connect(_ link: String) { scannerPaused = true planAndConnect( @@ -765,7 +830,7 @@ struct ScannerInView: View { } -private func linkTextView(_ link: String) -> some View { +func linkTextView(_ link: String) -> some View { Text(link) .lineLimit(1) .font(.caption) @@ -916,11 +981,13 @@ private func showAskCurrentOrIncognitoProfileSheet( actionStyle: UIAlertAction.Style = .default, connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan?, + ownerVerification: OwnerVerification? = nil, dismiss: Bool, cleanup: (() -> Void)? ) { showSheet( title, + message: ownerVerificationMessage(ownerVerification), actions: {[ UIAlertAction( title: NSLocalizedString("Use current profile", comment: "new chat action"), @@ -1056,6 +1123,7 @@ private func showOwnGroupLinkConfirmConnectSheet( private func showPrepareContactAlert( connectionLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, + ownerVerification: OwnerVerification? = nil, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)? @@ -1074,6 +1142,7 @@ private func showPrepareContactAlert( size: alertProfileImageSize ), theme: theme, + information: ownerVerificationMessage(ownerVerification), cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"), confirmTitle: NSLocalizedString("Open new chat", comment: "new chat action"), onCancel: { cleanup?() }, @@ -1101,6 +1170,7 @@ private func showPrepareGroupAlert( connectionLink: CreatedConnLink, groupShortLinkInfo: GroupShortLinkInfo?, groupShortLinkData: GroupShortLinkData, + ownerVerification: OwnerVerification? = nil, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)? @@ -1120,6 +1190,7 @@ private func showPrepareGroupAlert( ), theme: theme, subtitle: isChannel ? subscriberCount : nil, + information: ownerVerificationMessage(ownerVerification), cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"), confirmTitle: isChannel ? NSLocalizedString("Open new channel", comment: "new chat action") @@ -1217,6 +1288,7 @@ private func showOpenKnownGroupAlert( // Spec: spec/client/navigation.md#planAndConnect func planAndConnect( _ shortOrFullLink: String, + linkOwnerSig: LinkOwnerSig? = nil, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)? = nil, @@ -1241,7 +1313,7 @@ func planAndConnect( func connectTask(_ inProgress: BoxedValue) { Task { - let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink, inProgress: inProgress) + let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink, linkOwnerSig: linkOwnerSig, inProgress: inProgress) await MainActor.run { ConnectProgressManager.shared.stopConnectProgress() } @@ -1250,13 +1322,14 @@ func planAndConnect( switch connectionPlan { case let .invitationLink(ilp): switch ilp { - case let .ok(contactSLinkData_): + case let .ok(contactSLinkData_, ownerVerification): if let contactSLinkData = contactSLinkData_ { logger.debug("planAndConnect, .invitationLink, .ok, short link data present") await MainActor.run { showPrepareContactAlert( connectionLink: connectionLink, contactShortLinkData: contactSLinkData, + ownerVerification: ownerVerification, theme: theme, dismiss: dismiss, cleanup: cleanup @@ -1269,6 +1342,7 @@ func planAndConnect( title: NSLocalizedString("Connect via one-time link", comment: "new chat sheet title"), connectionLink: connectionLink, connectionPlan: connectionPlan, + ownerVerification: ownerVerification, dismiss: dismiss, cleanup: cleanup ) @@ -1311,13 +1385,14 @@ func planAndConnect( } case let .contactAddress(cap): switch cap { - case let .ok(contactSLinkData_): + case let .ok(contactSLinkData_, ownerVerification): if let contactSLinkData = contactSLinkData_ { logger.debug("planAndConnect, .contactAddress, .ok, short link data present") await MainActor.run { showPrepareContactAlert( connectionLink: connectionLink, contactShortLinkData: contactSLinkData, + ownerVerification: ownerVerification, theme: theme, dismiss: dismiss, cleanup: cleanup @@ -1330,6 +1405,7 @@ func planAndConnect( title: NSLocalizedString("Connect via contact address", comment: "new chat sheet title"), connectionLink: connectionLink, connectionPlan: connectionPlan, + ownerVerification: ownerVerification, dismiss: dismiss, cleanup: cleanup ) @@ -1389,7 +1465,7 @@ func planAndConnect( } case let .groupLink(glp): switch glp { - case let .ok(groupShortLinkInfo_, groupSLinkData_): + case let .ok(groupShortLinkInfo_, groupSLinkData_, ownerVerification): if let groupSLinkData = groupSLinkData_ { logger.debug("planAndConnect, .groupLink, .ok, short link data present") await MainActor.run { @@ -1397,6 +1473,7 @@ func planAndConnect( connectionLink: connectionLink, groupShortLinkInfo: groupShortLinkInfo_, groupShortLinkData: groupSLinkData, + ownerVerification: ownerVerification, theme: theme, dismiss: dismiss, cleanup: cleanup @@ -1409,6 +1486,7 @@ func planAndConnect( title: NSLocalizedString("Join group", comment: "new chat sheet title"), connectionLink: connectionLink, connectionPlan: connectionPlan, + ownerVerification: ownerVerification, dismiss: dismiss, cleanup: cleanup ) @@ -1454,6 +1532,33 @@ func planAndConnect( showOpenKnownGroupAlert(groupInfo, theme: theme, dismiss: dismiss) } } + case let .noRelays(groupSLinkData_): + logger.debug("planAndConnect, .groupLink, .noRelays") + await MainActor.run { + if let groupSLinkData = groupSLinkData_ { + showOpenChatAlert( + profileName: groupSLinkData.groupProfile.displayName, + profileFullName: groupSLinkData.groupProfile.fullName, + profileImage: + ProfileImage( + imageStr: groupSLinkData.groupProfile.image, + iconName: "antenna.radiowaves.left.and.right.circle.fill", + size: alertProfileImageSize + ), + theme: theme, + subtitle: NSLocalizedString("Channel has no active relays. Please try to join later.", comment: "alert subtitle"), + cancelTitle: NSLocalizedString("OK", comment: "alert button"), + confirmTitle: nil, + onCancel: { cleanup?() } + ) + } else { + showAlert( + NSLocalizedString("Channel temporarily unavailable", comment: "alert title"), + message: NSLocalizedString("Channel has no active relays. Please try to join later.", comment: "alert message") + ) + cleanup?() + } + } } case let .error(chatError): logger.debug("planAndConnect, .error \(chatErrorString(chatError))") @@ -1602,6 +1707,14 @@ private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType? } } +private func ownerVerificationMessage(_ ov: OwnerVerification?) -> String? { + switch ov { + case .verified: NSLocalizedString("Link signature verified.", comment: "owner verification") + case let .failed(reason): String.localizedStringWithFormat(NSLocalizedString("⚠️ Signature verification failed: %@.", comment: "owner verification"), reason) + case .none: nil + } +} + func connReqSentAlert(_ type: ConnReqType) -> Alert { return mkAlert( title: "Connection request sent!", diff --git a/apps/ios/Shared/Views/NewChat/OnboardingCards.swift b/apps/ios/Shared/Views/NewChat/OnboardingCards.swift new file mode 100644 index 0000000000..0a0b3c143d --- /dev/null +++ b/apps/ios/Shared/Views/NewChat/OnboardingCards.swift @@ -0,0 +1,311 @@ +// +// OnboardingCards.swift +// SimpleX (iOS) +// +// Created by simplex-chat on 06.04.2026. +// Copyright © 2026 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +// MARK: - Card component + +struct OnboardingCardView: View { + @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + let imageName: String + let icon: String + let title: LocalizedStringKey + var subtitle: LocalizedStringKey? = nil + let labelHeightRatio: CGFloat + let action: () -> Void + + static let lightStops: [Gradient.Stop] = [ + .init(color: oklch(0.9219, 0.0431, 249.4), location: 0.0), + .init(color: oklch(0.9198, 0.0471, 240.7), location: 0.5), + .init(color: oklch(0.9772, 0.0358, 196.6), location: 0.9), + .init(color: oklch(0.9829, 0.0104, 70.0), location: 0.95), + .init(color: oklch(0.9886, 0.0272, 99.1), location: 1.0) + ] + + static let darkStops: [Gradient.Stop] = [ + .init(color: oklch(0.1578, 0.0609, 267.3), location: 0.4), + .init(color: oklch(0.4729, 0.1574, 267.3), location: 0.72), + .init(color: oklch(0.9024, 0.0760, 202.8), location: 0.9), + .init(color: oklch(0.9384, 0.0354, 65.0), location: 0.95), + .init(color: oklch(0.9744, 0.0370, 88.4), location: 1.0) + ] + + static let gradientAngle: Double = 80.0 * .pi / 180.0 + + static func gradientPoints(aspectRatio: CGFloat, scale: CGFloat) -> (start: UnitPoint, end: UnitPoint) { + let r = Double(aspectRatio) + let s = Double(scale) + let dx = cos(gradientAngle) + let dy = -sin(gradientAngle) / r + let dLenSq = dx * dx + dy * dy + let projections = [ + -0.5 * dx + (-0.5) * dy, + 0.5 * dx + (-0.5) * dy, + -0.5 * dx + 0.5 * dy, + 0.5 * dx + 0.5 * dy + ] + let tMin = projections.min()! + let tMax = projections.max()! + let startX = 0.5 + tMin * dx / dLenSq + let startY = 0.5 + tMin * dy / dLenSq + let endX = 0.5 + tMax * dx / dLenSq + let endY = 0.5 + tMax * dy / dLenSq + return ( + start: .init(x: 0.5 + (startX - 0.5) * s, y: 0.5 + (startY - 0.5) * s), + end: .init(x: 0.5 + (endX - 0.5) * s, y: 0.5 + (endY - 0.5) * s) + ) + } + + var body: some View { + Button(action: action) { + GeometryReader { geo in + let labelHeight = geo.size.width * labelHeightRatio + let imageHeight = max(geo.size.height - labelHeight, 1) + let imageAspect = imageHeight / geo.size.width + let gp = Self.gradientPoints(aspectRatio: imageAspect, scale: colorScheme == .light ? 1.2 : 1.5) + VStack(spacing: 0) { + ZStack { + LinearGradient( + stops: colorScheme == .light ? Self.lightStops : Self.darkStops, + startPoint: gp.start, + endPoint: gp.end + ) + #if SIMPLEX_ASSETS + Image(colorScheme == .light ? imageName : "\(imageName)-light") + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + #else + Image(systemName: icon) + .font(.system(size: imageHeight * 0.25)) + .foregroundColor(theme.colors.primary) + #endif + } + .frame(height: imageHeight) + + labelRow(height: labelHeight) + } + } + .clipShape(RoundedRectangle(cornerRadius: 24)) + } + .buttonStyle(.plain) + } + + private func labelRow(height: CGFloat) -> some View { + VStack { + HStack { + #if SIMPLEX_ASSETS + Image(systemName: icon) + .font(.system(size: 24)) + .foregroundColor(theme.colors.primary) + #endif + Text(title) + .font(.body) + .fontWeight(.medium) + .foregroundColor(theme.colors.onBackground) + .lineLimit(1) + .minimumScaleFactor(0.75) + } + if let subtitle { + Text(subtitle) + .font(.footnote) + .foregroundColor(theme.colors.onBackground.opacity(0.7)) + } + } + .frame(height: height) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.horizontal, 16) + .background(ToolbarMaterial.material(toolbarMaterial)) + } +} + +// MARK: - Onboarding pager + +private let backButtonHeight: CGFloat = 44 + +struct ConnectOnboardingView: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @Environment(\.verticalSizeClass) private var verticalSizeClass + @State private var currentPage = 0 + @State private var showConnectViaLink = false + @State private var showInviteSomeone = false + @State private var showCreateAddress = false + + var body: some View { + TabView(selection: $currentPage) { + talkToSomeonePage.tag(0) + connectWithSomeonePage.tag(1) + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .sheet(isPresented: $showConnectViaLink) { + NavigationView { + NewChatView(selection: .connect, showQRCodeScanner: true, onboarding: true) + .modifier(ThemedBackground(grouped: true)) + } + .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + } + .sheet(isPresented: $showInviteSomeone) { + NavigationView { + NewChatView(selection: .invite, onboarding: true) + .modifier(ThemedBackground(grouped: true)) + } + .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + } + .sheet(isPresented: $showCreateAddress) { + NavigationView { + UserAddressView(autoCreate: true, onboarding: true) + .modifier(ThemedBackground(grouped: true)) + } + .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + } + } + + @ViewBuilder + private func cardPair( + _ geo: GeometryProxy, + @ViewBuilder card1: () -> C1, + @ViewBuilder card2: () -> C2 + ) -> some View { + let padding: CGFloat = 20 + let spacing: CGFloat = 20 + let isLandscape = verticalSizeClass == .compact + let cardWidth = isLandscape + ? (geo.size.width - padding * 2 - spacing) / 2 + : geo.size.width - padding * 2 + let maxCardHeight = cardWidth * 0.75 + + if isLandscape { + HStack(spacing: spacing) { + card1().frame(maxHeight: maxCardHeight) + card2().frame(maxHeight: maxCardHeight) + } + .padding(.horizontal, padding) + } else { + VStack(spacing: spacing) { + card1().frame(maxHeight: maxCardHeight) + card2().frame(maxHeight: maxCardHeight) + } + .padding(.horizontal, padding) + } + } + + // MARK: Screen 1 + + @ViewBuilder + private func pageHeader(_ title: LocalizedStringKey, showBack: Bool) -> some View { + let isLandscape = verticalSizeClass == .compact + let titleView = Text(title) + .font(.largeTitle) + .bold() + .lineLimit(1) + .minimumScaleFactor(0.67) + .frame(maxWidth: .infinity, alignment: .center) + if isLandscape { + ZStack(alignment: .leading) { + if showBack { backButton } + titleView + } + .padding(.horizontal, 16) + } else { + VStack(spacing: 0) { + if showBack { + backButton.frame(maxWidth: .infinity, alignment: .leading) + } else { + Color.clear.frame(height: backButtonHeight) + } + titleView + } + .padding(.horizontal, 16) + } + } + + private var backButton: some View { + Button { + withAnimation { currentPage = 0 } + } label: { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + Text("Back") + } + } + .frame(height: backButtonHeight) + } + + // MARK: Screen 1 + + private var talkToSomeonePage: some View { + GeometryReader { geo in + VStack(spacing: 0) { + pageHeader("Talk to someone", showBack: false) + + Spacer(minLength: 16) + + cardPair(geo) { + OnboardingCardView( + imageName: "card-let-someone-connect-to-you-alpha", + icon: "link.badge.plus", + title: "Let someone connect to you", + labelHeightRatio: 0.132, + action: { withAnimation { currentPage = 1 } } + ) + } card2: { + OnboardingCardView( + imageName: "card-connect-via-link-alpha", + icon: "qrcode.viewfinder", + title: "Connect via link or QR code", + labelHeightRatio: 0.132, + action: { showConnectViaLink = true } + ) + } + + Spacer(minLength: 16) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + + // MARK: Screen 2 + + private var connectWithSomeonePage: some View { + GeometryReader { geo in + VStack(spacing: 0) { + pageHeader("Create your link", showBack: true) + + Spacer(minLength: 16) + + cardPair(geo) { + OnboardingCardView( + imageName: "card-invite-someone-privately-alpha", + icon: "link.badge.plus", + title: "Invite someone privately", + subtitle: "A link for one person to connect", + labelHeightRatio: 0.195, + action: { showInviteSomeone = true } + ) + } card2: { + OnboardingCardView( + imageName: "card-create-your-public-address-alpha", + icon: "qrcode", + title: m.userAddress != nil ? "Your public address" : "Create your public address", + subtitle: "For anyone to reach you", + labelHeightRatio: 0.195, + action: { showCreateAddress = true } + ) + } + + Spacer(minLength: 16) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } +} diff --git a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift deleted file mode 100644 index f22d59fcac..0000000000 --- a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// AddressCreationCard.swift -// SimpleX (iOS) -// -// Created by Diogo Cunha on 13/11/2024. -// Copyright © 2024 SimpleX Chat. All rights reserved. -// -// Spec: spec/client/navigation.md - -import SwiftUI -import SimpleXChat - -struct AddressCreationCard: View { - @EnvironmentObject var theme: AppTheme - @EnvironmentObject private var chatModel: ChatModel - @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize - @AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false - @State private var showAddressCreationAlert = false - @State private var showAddressSheet = false - @State private var showAddressInfoSheet = false - - var body: some View { - let addressExists = chatModel.userAddress != nil - let chats = chatModel.chats.filter { chat in - !chat.chatInfo.chatDeleted && !chat.chatInfo.contactCard - } - ZStack(alignment: .topTrailing) { - HStack(alignment: .top, spacing: 16) { - let envelopeSize = dynamicSize(userFont).profileImageSize - Image(systemName: "envelope.circle.fill") - .resizable() - .frame(width: envelopeSize, height: envelopeSize) - .foregroundColor(.accentColor) - VStack(alignment: .leading) { - Text("Your SimpleX address") - .font(.title3) - Spacer() - Text("How to use it") + textSpace + Text(Image(systemName: "info.circle")).foregroundColor(theme.colors.secondary) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - VStack(alignment: .trailing) { - Image(systemName: "multiply") - .foregroundColor(theme.colors.secondary) - .onTapGesture { - showAddressCreationAlert = true - } - Spacer() - Text("Create") - .foregroundColor(.accentColor) - .onTapGesture { - showAddressSheet = true - } - } - } - .onTapGesture { - showAddressInfoSheet = true - } - .padding() - .background(theme.appColors.sentMessage) - .cornerRadius(12) - .frame(height: dynamicSize(userFont).rowHeight) - .alert(isPresented: $showAddressCreationAlert) { - Alert( - title: Text("SimpleX address"), - message: Text("Tap Create SimpleX address in the menu to create it later."), - dismissButton: .default(Text("Ok")) { - withAnimation { - addressCreationCardShown = true - } - } - ) - } - .sheet(isPresented: $showAddressSheet) { - NavigationView { - UserAddressView(autoCreate: true) - .navigationTitle("SimpleX address") - .navigationBarTitleDisplayMode(.large) - .modifier(ThemedBackground(grouped: true)) - } - } - .sheet(isPresented: $showAddressInfoSheet) { - NavigationView { - UserAddressLearnMore(showCreateAddressButton: true) - .navigationTitle("Address or 1-time link?") - .navigationBarTitleDisplayMode(.inline) - .modifier(ThemedBackground(grouped: true)) - } - } - .onChange(of: addressExists) { exists in - if exists, !addressCreationCardShown { - addressCreationCardShown = true - } - } - .onChange(of: chats.count) { size in - if size >= 3, !addressCreationCardShown { - addressCreationCardShown = true - } - } - .onAppear { - if addressExists, !addressCreationCardShown { - addressCreationCardShown = true - } - } - } -} - -#Preview { - AddressCreationCard() -} diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index b5598c1f85..b61b81a46b 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -44,160 +44,147 @@ struct OnboardingButtonStyle: ButtonStyle { } } -private enum OnboardingConditionsViewSheet: Identifiable { - case showConditions - case configureOperators - - var id: String { - switch self { - case .showConditions: return "showConditions" - case .configureOperators: return "configureOperators" - } - } -} - struct OnboardingConditionsView: View { @EnvironmentObject var theme: AppTheme - @State private var serverOperators: [ServerOperator] = [] - @State private var selectedOperatorIds = Set() - @State private var sheetItem: OnboardingConditionsViewSheet? = nil - @State private var notificationsModeNavLinkActive = false - @State private var justOpened = true + @Environment(\.colorScheme) var colorScheme: ColorScheme + @State private var showConditionsSheet = false + var selectedOperatorIds: Set var body: some View { GeometryReader { g in - let v = ScrollView { - VStack(alignment: .leading, spacing: 20) { - Text("Conditions of use") - .font(.largeTitle) - .bold() - .frame(maxWidth: .infinity, alignment: .center) - .padding(.top, 25) + VStack(alignment: .leading, spacing: 10) { + Spacer(minLength: 0) - Spacer() + heroImage().frame(maxWidth: .infinity, minHeight: 80) - VStack(alignment: .leading, spacing: 20) { - Text("Private chats, groups and your contacts are not accessible to server operators.") - .lineSpacing(2) - .frame(maxWidth: .infinity, alignment: .leading) - Text(""" - By using SimpleX Chat you agree to: - - send only legal content in public groups. - - respect other users – no spam. - """) - .lineSpacing(2) - .frame(maxWidth: .infinity, alignment: .leading) + Text("Network commitments") + .font(.largeTitle) + .bold() + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + .fixedSize(horizontal: false, vertical: true) - Button("Privacy policy and conditions of use.") { - sheetItem = .showConditions - } - .frame(maxWidth: .infinity, alignment: .leading) - } - .padding(.horizontal, 4) + Text("Operators commit to:\n- Be independent\n- Minimize metadata usage\n- Run verified open-source code") + .font(.callout) + .lineSpacing(2) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.leading, 4) + .padding(.top, 10) + .fixedSize(horizontal: false, vertical: true) - Spacer() + Text("You commit to:\n- Only legal content in public groups\n- Respect other users - no spam") + .font(.callout) + .lineSpacing(2) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.leading, 4) + .padding(.top, 10) + .fixedSize(horizontal: false, vertical: true) - VStack(spacing: 12) { - acceptConditionsButton() - - Button("Configure server operators") { - sheetItem = .configureOperators - } - .frame(minHeight: 40) - } + Button { + showConditionsSheet = true + } label: { + Text("Privacy policy and conditions of use.") + .fontWeight(.medium) + .fixedSize(horizontal: false, vertical: true) } - .padding(25) - .frame(minHeight: g.size.height) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.leading, 4) + .padding(.top, 10) + .padding(.bottom, 15) + + Spacer(minLength: 0) + + acceptButton() + .padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0) } - .onAppear { - if justOpened { - serverOperators = ChatModel.shared.conditions.serverOperators - selectedOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId }) - justOpened = false + .padding(.horizontal, 25) + .padding(.top, 25) + .padding(.bottom, 25) + .frame(minHeight: g.size.height) + } + .frame(maxHeight: .infinity) + .navigationBarHidden(true) + .sheet(isPresented: $showConditionsSheet) { + NavigationView { + VStack { + ConditionsTextView() + .padding() + acceptButton() + .padding(.horizontal, 25) + .padding(.bottom, 20) } - } - .sheet(item: $sheetItem) { item in - switch item { - case .showConditions: - SimpleConditionsView() - .modifier(ThemedBackground(grouped: true)) - case .configureOperators: - ChooseServerOperators(serverOperators: serverOperators, selectedOperatorIds: $selectedOperatorIds) - .modifier(ThemedBackground()) - } - } - .frame(maxHeight: .infinity, alignment: .top) - if #available(iOS 16.4, *) { - v.scrollBounceBehavior(.basedOnSize) - } else { - v + .navigationTitle("Conditions of use") + .navigationBarTitleDisplayMode(.large) + .toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) } + .modifier(ThemedBackground(grouped: true)) } } - .frame(maxHeight: .infinity, alignment: .top) - .navigationBarHidden(true) // necessary on iOS 15 } - private func continueToNextStep() { - onboardingStageDefault.set(.step4_SetNotificationsMode) - notificationsModeNavLinkActive = true - } - - func notificationsModeNavLinkButton(_ button: @escaping (() -> some View)) -> some View { + @ViewBuilder + private func heroImage() -> some View { + #if SIMPLEX_ASSETS + Image(colorScheme == .light ? "network-commitments" : "network-commitments-light") + .resizable() + .scaledToFit() + #else ZStack { - button() - - NavigationLink(isActive: $notificationsModeNavLinkActive) { - notificationsModeDestinationView() - } label: { - EmptyView() - } - .frame(width: 1, height: 1) - .hidden() + let gp = OnboardingCardView.gradientPoints(aspectRatio: 1.5, scale: colorScheme == .light ? 1.2 : 1.5) + LinearGradient( + stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops, + startPoint: gp.start, + endPoint: gp.end + ) + Image(systemName: "checkmark.shield") + .font(.system(size: 72)) + .foregroundColor(theme.colors.primary) } + .aspectRatio(1.5, contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 24)) + .padding(.horizontal, 25) + #endif } - private func notificationsModeDestinationView() -> some View { - SetNotificationsMode() - .navigationBarBackButtonHidden(true) - .modifier(ThemedBackground()) - } - - private func acceptConditionsButton() -> some View { - notificationsModeNavLinkButton { - Button { - Task { - do { - let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId - let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: Array(selectedOperatorIds)) + private func acceptButton() -> some View { + Button { + Task { + do { + let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId + let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: Array(selectedOperatorIds)) + await MainActor.run { + ChatModel.shared.conditions = r + } + if let enabledOps = enabledOperators(r.serverOperators) { + let r2 = try await setServerOperators(operators: enabledOps) await MainActor.run { - ChatModel.shared.conditions = r + ChatModel.shared.conditions = r2 + completeOnboarding() } - if let enabledOperators = enabledOperators(r.serverOperators) { - let r2 = try await setServerOperators(operators: enabledOperators) - await MainActor.run { - ChatModel.shared.conditions = r2 - continueToNextStep() - } - } else { - await MainActor.run { - continueToNextStep() - } - } - } catch let error { + } else { await MainActor.run { - showAlert( - NSLocalizedString("Error accepting conditions", comment: "alert title"), - message: responseError(error) - ) + completeOnboarding() } } + } catch let error { + await MainActor.run { + showAlert( + NSLocalizedString("Error accepting conditions", comment: "alert title"), + message: responseError(error) + ) + } } - } label: { - Text("Accept") } - .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) - .disabled(selectedOperatorIds.isEmpty) + } label: { + Text("Accept") } + .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) + .disabled(selectedOperatorIds.isEmpty) + } + + private func completeOnboarding() { + let m = ChatModel.shared + onboardingStageDefault.set(.onboardingComplete) + m.onboardingStage = .onboardingComplete } private func enabledOperators(_ operators: [ServerOperator]) -> [ServerOperator]? { @@ -222,7 +209,7 @@ struct OnboardingConditionsView: View { if !haveXFTPProxy { op.xftpRoles.proxy = true } ops[firstEnabledIndex] = op return ops - } else { // Shouldn't happen - view doesn't let to proceed if no operators are enabled + } else { return nil } } else { @@ -405,5 +392,5 @@ struct ChooseServerOperatorsInfoView: View { } #Preview { - OnboardingConditionsView() + OnboardingConditionsView(selectedOperatorIds: []) } diff --git a/apps/ios/Shared/Views/Onboarding/ConnectBannerCard.swift b/apps/ios/Shared/Views/Onboarding/ConnectBannerCard.swift new file mode 100644 index 0000000000..87f66a72bb --- /dev/null +++ b/apps/ios/Shared/Views/Onboarding/ConnectBannerCard.swift @@ -0,0 +1,114 @@ +// +// ConnectBannerCard.swift +// SimpleX (iOS) +// +// Copyright © 2026 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +private let bannerImageRatio: CGFloat = 800 / 505 + +struct ConnectBannerCard: View { + @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + @AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false + @State private var showNewLink = false + @State private var showPasteLink = false + + var body: some View { + VStack(alignment: .trailing, spacing: 3) { + Button { + withAnimation { addressCreationCardShown = true } + } label: { + Image(systemName: "multiply") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(theme.colors.secondary) + .frame(width: 30, height: 30) + .background(theme.colors.onBackground.opacity(0.08), in: Circle()) + } + HStack(spacing: 2) { + bannerHalf( + imageName: "banner-create-link", + icon: "link.badge.plus", + title: "New 1-time link", + action: { showNewLink = true } + ) + bannerHalf( + imageName: "banner-paste-link", + icon: "qrcode.viewfinder", + title: "Paste link / Scan", + action: { showPasteLink = true } + ) + } + .clipShape(RoundedRectangle(cornerRadius: 18)) + } + .sheet(isPresented: $showNewLink) { + NavigationView { + NewChatView(selection: .invite) + .modifier(ThemedBackground(grouped: true)) + } + } + .sheet(isPresented: $showPasteLink) { + NavigationView { + NewChatView(selection: .connect, showQRCodeScanner: true) + .modifier(ThemedBackground(grouped: true)) + } + } + } + + @ViewBuilder + private func bannerHalf(imageName: String, icon: String, title: LocalizedStringKey, action: @escaping () -> Void) -> some View { + Button(action: action) { + VStack(spacing: 0) { + #if SIMPLEX_ASSETS + Image(colorScheme == .light ? imageName : "\(imageName)-light") + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) + #else + gradientFallback(icon: icon) + #endif + HStack(spacing: 8) { + #if SIMPLEX_ASSETS + Image(systemName: icon) + .font(.system(size: 18)) + .foregroundColor(theme.colors.primary) + #endif + Text(title) + .font(.footnote) + .foregroundColor(theme.colors.onBackground) + .lineLimit(1) + .minimumScaleFactor(0.75) + } + .frame(height: 20) + .frame(maxWidth: .infinity) + .padding(.vertical, 8) + .background(ToolbarMaterial.material(toolbarMaterial)) + } + } + .buttonStyle(.plain) + } + + @ViewBuilder + private func gradientFallback(icon: String) -> some View { + let gp = OnboardingCardView.gradientPoints( + aspectRatio: 1 / bannerImageRatio, + scale: colorScheme == .light ? 1.2 : 1.5 + ) + ZStack { + LinearGradient( + stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops, + startPoint: gp.start, + endPoint: gp.end + ) + Image(systemName: icon) + .font(.system(size: 40)) + .foregroundColor(theme.colors.primary) + } + .aspectRatio(bannerImageRatio, contentMode: .fit) + .frame(maxWidth: .infinity) + } +} diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 7301c0421d..3c33546436 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -29,45 +29,82 @@ enum UserProfileAlert: Identifiable { let MAX_BIO_LENGTH_BYTES = 160 struct CreateProfile: View { + @Environment(\.colorScheme) var colorScheme @Environment(\.dismiss) var dismiss @EnvironmentObject var theme: AppTheme @State private var displayName: String = "" @State private var profileBio: String = "" @FocusState private var focusDisplayName @State private var alert: UserProfileAlert? + @State private var showChooseSource = false + @State private var showImagePicker = false + @State private var showTakePhoto = false + @State private var chosenImage: UIImage? = nil + @State private var profileImage: String? = nil var body: some View { List { + Group { + HStack(spacing: 0) { + Spacer(minLength: 0) + ZStack(alignment: .center) { + ZStack(alignment: .topTrailing) { + ProfileImage(imageStr: profileImage, size: 128) + if profileImage != nil { + Button { + profileImage = nil + } label: { + Image(systemName: "multiply") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 12) + } + } + } + + editImageButton { showChooseSource = true } + .buttonStyle(BorderlessButtonStyle()) + } + .padding(.horizontal, 10) // Offsets transparent space built into 3D asset + Spacer(minLength: 0) + #if SIMPLEX_ASSETS + Image(colorScheme == .light ? "create-profile" : "create-profile-light") + .resizable() + .scaledToFit() + .frame(height: 140) + // No trailing spacer — asset image has empty space on the right + #endif + } + } + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0)) + Section { - TextField("Enter your name…", text: $displayName) - .focused($focusDisplayName) - TextField("Bio", text: $profileBio) - Button { - createProfile() - } label: { - Label("Create profile", systemImage: "checkmark") + ZStack(alignment: .leading) { + let name = displayName.trimmingCharacters(in: .whitespaces) + if name != mkValidName(name) { + Button { + alert = .invalidNameError(validName: mkValidName(name)) + } label: { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } + } else { + Image(systemName: "pencil").foregroundColor(theme.colors.secondary) + } + TextField("Enter your name…", text: $displayName) + .padding(.leading, 36) + .focused($focusDisplayName) + } + ZStack(alignment: .leading) { + Image(systemName: "pencil").foregroundColor(theme.colors.secondary) + TextField("Bio", text: $profileBio) + .padding(.leading, 36) + } + Button(action: createProfile) { + settingsRow("checkmark", color: theme.colors.primary) { Text("Create profile") } } .disabled(!canCreateProfile(displayName) || !bioFitsLimit()) - } header: { - HStack { - Text("Your profile") - .foregroundColor(theme.colors.secondary) - - let name = displayName.trimmingCharacters(in: .whitespaces) - let validName = mkValidName(name) - if name != validName { - Spacer() - validationErrorIndicator { - alert = .invalidNameError(validName: validName) - } - } else if !bioFitsLimit() { - Spacer() - validationErrorIndicator { - showAlert(NSLocalizedString("Bio too large", comment: "alert title")) - } - } - } - .frame(height: 20) } footer: { VStack(alignment: .leading, spacing: 8) { Text("Your profile is stored on your device and only shared with your contacts.") @@ -75,10 +112,42 @@ struct CreateProfile: View { .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity, alignment: .leading) } + .compactSectionSpacing() } .navigationTitle("Create your profile") .modifier(ThemedBackground(grouped: true)) .alert(item: $alert) { a in userProfileAlert(a, $displayName) } + .confirmationDialog("Profile image", isPresented: $showChooseSource, titleVisibility: .visible) { + Button("Take picture") { + showTakePhoto = true + } + Button("Choose from library") { + showImagePicker = true + } + } + .fullScreenCover(isPresented: $showTakePhoto) { + ZStack { + Color.black.edgesIgnoringSafeArea(.all) + CameraImagePicker(image: $chosenImage) + } + } + .sheet(isPresented: $showImagePicker) { + LibraryImagePicker(image: $chosenImage) { _ in + await MainActor.run { + showImagePicker = false + } + } + } + .onChange(of: chosenImage) { image in + Task { + let resized: String? = if let image { + await resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) + } else { + nil + } + await MainActor.run { profileImage = resized } + } + } .onAppear() { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { focusDisplayName = true @@ -86,14 +155,6 @@ struct CreateProfile: View { } } - private func validationErrorIndicator(_ onTap: @escaping () -> Void) -> some View { - Image(systemName: "exclamationmark.circle") - .foregroundColor(.red) - .onTapGesture { - onTap() - } - } - private func bioFitsLimit() -> Bool { chatJsonLength(profileBio) <= MAX_BIO_LENGTH_BYTES } @@ -104,7 +165,8 @@ struct CreateProfile: View { let profile = Profile( displayName: displayName.trimmingCharacters(in: .whitespaces), fullName: "", - shortDescr: shortDescr + shortDescr: shortDescr, + image: profileImage ) let m = ChatModel.shared do { @@ -133,61 +195,103 @@ struct CreateProfile: View { struct CreateFirstProfile: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme - @Environment(\.dismiss) var dismiss + @Environment(\.colorScheme) var colorScheme: ColorScheme @State private var displayName: String = "" @FocusState private var focusDisplayName @State private var nextStepNavLinkActive = false - + @State private var showMigrateSheet = false var body: some View { - let v = VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .center, spacing: 16) { - Text("Create profile") - .font(.largeTitle) - .bold() - .multilineTextAlignment(.center) - - Text("Your profile is stored on your device and only shared with your contacts.") - .font(.callout) - .foregroundColor(theme.colors.secondary) - .multilineTextAlignment(.center) - } - .fixedSize(horizontal: false, vertical: true) - .frame(maxWidth: .infinity) // Ensures it takes up the full width - .padding(.horizontal, 10) - .onTapGesture { focusDisplayName = false } - - HStack { - let name = displayName.trimmingCharacters(in: .whitespaces) - let validName = mkValidName(name) - ZStack(alignment: .trailing) { - TextField("Enter your name…", text: $displayName) - .focused($focusDisplayName) - .padding(.horizontal) - .padding(.trailing, 20) - .padding(.vertical, 10) - .background( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .fill(Color(uiColor: .tertiarySystemFill)) + let spacing: CGFloat = 10 + let topPadding: CGFloat = 8 + let padding: CGFloat = 25 + GeometryReader { g in + let v = ScrollView { + VStack(alignment: .center, spacing: spacing) { + #if SIMPLEX_ASSETS + Image(colorScheme == .light ? "your-profile" : "your-profile-light") + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) + #else + ZStack { + let gp = OnboardingCardView.gradientPoints(aspectRatio: 1.0, scale: colorScheme == .light ? 1.2 : 1.5) + LinearGradient( + stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops, + startPoint: gp.start, + endPoint: gp.end ) - if name != validName { - Button { - showAlert(.invalidNameError(validName: validName)) - } label: { - Image(systemName: "exclamationmark.circle") - .foregroundColor(.red) - .padding(.horizontal, 10) - } + Image(systemName: "person.crop.rectangle") + .font(.system(size: 72)) + .foregroundColor(theme.colors.primary) } + .aspectRatio(1.0, contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 24)) + .padding(.horizontal, 25) + .frame(maxWidth: .infinity) + #endif + + Text("Your profile") + .font(.largeTitle) + .bold() + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + + Text("On your phone, not on servers.") + .font(.title3) + .fontWeight(.medium) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + + Text("No account. No phone. No email. No ID.\nThe most secure encryption.") + .font(.footnote) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + + profileNameField() + .padding(.top) + .padding(.bottom, 5) + + Spacer(minLength: 0) + + createProfileButton() + .padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0) + } + .padding(.horizontal, padding) + .padding(.top, topPadding) + .padding(.bottom, padding) + .frame(minHeight: g.size.height) + } + .onTapGesture { focusDisplayName = false } + .sheet(isPresented: $showMigrateSheet, onDismiss: { m.migrationState = nil }) { + NavigationView { + MigrateToDevice(migrationState: $m.migrationState) + .navigationTitle("Migrate here") + .modifier(ThemedBackground(grouped: true)) } } - .padding(.top) - - Spacer() - - VStack(spacing: 10) { - createProfileButton() - if !focusDisplayName { - onboardingButtonPlaceholder() + if #available(iOS 17, *) { + v.scrollBounceBehavior(.basedOnSize).defaultScrollAnchor(.bottom) + } else if #available(iOS 16.4, *) { + v.scrollBounceBehavior(.basedOnSize) + } else { + v + } + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + if m.migrationState == nil { + m.migrationState = .pasteOrScanLink + } + showMigrateSheet = true + } label: { + HStack(spacing: 4) { + Image(systemName: "tray.and.arrow.down") + Text("Migrate") + .fontWeight(.medium) + } } } } @@ -195,23 +299,40 @@ struct CreateFirstProfile: View { if #available(iOS 16, *) { focusDisplayName = true } else { - // it does not work before animation completes on iOS 15 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { focusDisplayName = true } } } - .padding(.horizontal, 25) - .padding(.bottom, 25) - .frame(maxWidth: .infinity, alignment: .leading) - if #available(iOS 16, *) { - return v.padding(.top, 10) - } else { - return v.padding(.top, 75).ignoresSafeArea(.all, edges: .top) + .frame(maxHeight: .infinity) + } + + private func profileNameField() -> some View { + let name = displayName.trimmingCharacters(in: .whitespaces) + let validName = mkValidName(name) + return ZStack(alignment: .trailing) { + TextField("Enter profile name...", text: $displayName) + .focused($focusDisplayName) + .padding(.horizontal) + .padding(.trailing, name != validName ? 20 : 0) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(Color(uiColor: .tertiarySystemFill)) + ) + if name != validName { + Button { + showAlert(.invalidNameError(validName: validName)) + } label: { + Image(systemName: "exclamationmark.circle") + .foregroundColor(.red) + .padding(.horizontal, 10) + } + } } } - func createProfileButton() -> some View { + private func createProfileButton() -> some View { ZStack { Button { createProfile() @@ -236,7 +357,7 @@ struct CreateFirstProfile: View { } private func nextStepDestinationView() -> some View { - OnboardingConditionsView() + YourNetworkView() .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) } diff --git a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift index 263b55a42d..e9b9c6b970 100644 --- a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift +++ b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift @@ -9,7 +9,7 @@ import SwiftUI -struct HowItWorks: View { +struct OldHowItWorks: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var m: ChatModel var onboarding: Bool @@ -28,7 +28,7 @@ struct HowItWorks: View { Text("Only client devices store user profiles, contacts, groups, and messages.") Text("All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.") if !onboarding { - Text("Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).") + ExternalLink("Read more in our GitHub repository.", destination: URL(string: "https://github.com/simplex-chat/simplex-chat#readme")!) } } .padding(.bottom) @@ -61,9 +61,57 @@ struct HowItWorks: View { } } -struct HowItWorks_Previews: PreviewProvider { +struct WhySimpleX: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var m: ChatModel + var onboarding: Bool + @Binding var createProfileNavLinkActive: Bool + + var body: some View { + VStack(alignment: .leading) { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + Text("You were born without an account") + .font(.title) + .bold() + .padding(.top) + Text("Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life.") + Text("Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible.") + Text("There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected.") + Text("Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign.") + Text("Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public.") + Text("The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it.") + Text("Because we destroyed the power to know who you are. So that your power can never be taken.") + Text("Be free in your network.") + } + } + .padding(.bottom, 16) + + Spacer() + + if onboarding { + createFirstProfileButton() + } + } + .padding(onboarding ? 25 : 16) + .frame(maxHeight: .infinity, alignment: .top) + .modifier(ThemedBackground()) + } + + private func createFirstProfileButton() -> some View { + Button { + dismiss() + createProfileNavLinkActive = true + } label: { + Text("Get started") + } + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) + } +} + +struct WhySimpleX_Previews: PreviewProvider { static var previews: some View { - HowItWorks( + WhySimpleX( onboarding: true, createProfileNavLinkActive: Binding.constant(false) ) diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift index daef95fbc6..39ccabce04 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -19,17 +19,18 @@ struct OnboardingView: View { case .step1_SimpleXInfo: SimpleXInfo(onboarding: true) .modifier(ThemedBackground()) - case .step2_CreateProfile: // deprecated + case .step2_CreateProfile: CreateFirstProfile() .modifier(ThemedBackground()) case .step3_CreateSimpleXAddress: // deprecated CreateSimpleXAddress() - case .step3_ChooseServerOperators: - OnboardingConditionsView() + case .step3_ChooseServerOperators, + .step4_SetNotificationsMode: // deprecated + YourNetworkView() .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) - case .step4_SetNotificationsMode: - SetNotificationsMode() + case .step4_NetworkCommitments: + OnboardingConditionsView(selectedOperatorIds: Set(ChatModel.shared.conditions.serverOperators.filter { $0.enabled }.map { $0.operatorId })) .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) case .onboardingComplete: EmptyView() @@ -45,10 +46,11 @@ func onboardingButtonPlaceholder() -> some View { // Spec: spec/client/navigation.md#onboardingStage enum OnboardingStage: String, Identifiable { case step1_SimpleXInfo - case step2_CreateProfile // deprecated + case step2_CreateProfile case step3_CreateSimpleXAddress // deprecated - case step3_ChooseServerOperators // changed to simplified conditions - case step4_SetNotificationsMode + case step3_ChooseServerOperators + case step4_SetNotificationsMode // deprecated + case step4_NetworkCommitments case onboardingComplete public var id: Self { self } diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift index 717405b03b..1a1f1bb68c 100644 --- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -11,45 +11,39 @@ import SwiftUI import SimpleXChat struct SetNotificationsMode: View { - @EnvironmentObject var m: ChatModel - @State private var notificationMode = NotificationsMode.instant - @State private var showAlert: NotificationAlert? - @State private var showInfo: Bool = false + @Environment(\.dismiss) var dismiss + @Binding var notificationMode: NotificationsMode + @State private var showInfo = false var body: some View { GeometryReader { g in - let v = ScrollView { + ScrollView { VStack(alignment: .center, spacing: 20) { Text("Push notifications") .font(.largeTitle) .bold() .padding(.top, 25) - - infoText() - + + Button { + showInfo = true + } label: { + Label("How it affects privacy", systemImage: "info.circle") + .font(.headline) + } + Spacer() ForEach(NotificationsMode.values) { mode in NtfModeSelector(mode: mode, selection: $notificationMode) } - + Spacer() - + VStack(spacing: 10) { Button { - if let token = m.deviceToken { - setNotificationsMode(token, notificationMode) - } else { - AlertManager.shared.showAlertMsg(title: "No device token!") - } - onboardingStageDefault.set(.onboardingComplete) - m.onboardingStage = .onboardingComplete + dismiss() } label: { - if case .off = notificationMode { - Text("Use chat") - } else { - Text("Enable notifications") - } + Text("OK") } .buttonStyle(OnboardingButtonStyle()) onboardingButtonPlaceholder() @@ -58,50 +52,11 @@ struct SetNotificationsMode: View { .padding(25) .frame(minHeight: g.size.height) } - if #available(iOS 16.4, *) { - v.scrollBounceBehavior(.basedOnSize) - } else { - v - } } .frame(maxHeight: .infinity) .sheet(isPresented: $showInfo) { NotificationsInfoView() } - .navigationBarHidden(true) // necessary on iOS 15 - } - - private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) { - switch mode { - case .off: - m.tokenStatus = .new - m.notificationMode = .off - default: - Task { - do { - let status = try await apiRegisterToken(token: token, notificationMode: mode) - await MainActor.run { - m.tokenStatus = status - m.notificationMode = mode - } - } catch let error { - let a = getErrorAlert(error, "Error enabling notifications") - AlertManager.shared.showAlertMsg( - title: a.title, - message: a.message - ) - } - } - } - } - - private func infoText() -> some View { - Button { - showInfo = true - } label: { - Label("How it affects privacy", systemImage: "info.circle") - .font(.headline) - } } } @@ -180,6 +135,6 @@ struct NotificationsInfoView: View { struct NotificationsModeView_Previews: PreviewProvider { static var previews: some View { - SetNotificationsMode() + SetNotificationsMode(notificationMode: .constant(.instant)) } } diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index 80f35c1190..15b8e05b5e 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -12,68 +12,84 @@ import SimpleXChat struct SimpleXInfo: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.colorScheme) var colorScheme: ColorScheme - @State private var showHowItWorks = false + @State private var showWhyBuilt = false @State private var createProfileNavLinkActive = false var onboarding: Bool var body: some View { GeometryReader { g in - let v = ScrollView { - VStack(alignment: .leading) { - VStack(alignment: .center, spacing: 10) { - Image(colorScheme == .light ? "logo" : "logo-light") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: g.size.width * 0.67) - .padding(.bottom, 8) - .padding(.leading, 4) - .frame(maxWidth: .infinity, minHeight: 48, alignment: .top) - - Button { - showHowItWorks = true - } label: { - Label("The future of messaging", systemImage: "info.circle") - .font(.headline) - } - } + VStack(alignment: .center, spacing: 10) { + Image(colorScheme == .light ? "logo" : "logo-light") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: (g.size.width - 50) * 0.55) + .padding(.leading, 4) + .frame(maxWidth: .infinity, minHeight: 48, alignment: .top) - Spacer() + #if SIMPLEX_ASSETS + Image(colorScheme == .light ? "intro" : "intro-light") + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) + #else + ZStack { + let gp = OnboardingCardView.gradientPoints(aspectRatio: 1.0, scale: colorScheme == .light ? 1.2 : 1.5) + LinearGradient( + stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops, + startPoint: gp.start, + endPoint: gp.end + ) + Image(systemName: "bubble.left.and.bubble.right") + .font(.system(size: 72)) + .foregroundColor(theme.colors.primary) + } + .aspectRatio(1.0, contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 24)) + .padding(.horizontal, 25) + .frame(maxWidth: .infinity) + #endif - VStack(alignment: .leading) { - onboardingInfoRow("privacy", "Privacy redefined", - "No user identifiers.", width: 48) - onboardingInfoRow("shield", "Immune to spam", - "You decide who can connect.", width: 46) - onboardingInfoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized", - "Anybody can host servers.", width: 46) - } - .padding(.leading, 16) + Text("Be free\nin your network") + .font(.largeTitle) + .bold() + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) - Spacer() + Text("Private and secure messaging.") + .font(.title3) + .fontWeight(.medium) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) - if onboarding { - VStack(spacing: 10) { - createFirstProfileButton() + Text("The first network where you own\nyour contacts and groups.") + .font(.footnote) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) - Button { - m.migrationState = .pasteOrScanLink - } label: { - Label("Migrate from another device", systemImage: "tray.and.arrow.down") - .font(.system(size: 17, weight: .semibold)) - .frame(minHeight: 40) - } - .frame(maxWidth: .infinity) - } + if onboarding { + Spacer(minLength: 0) + + createFirstProfileButton() + .padding(.vertical, 10) + + Button { + showWhyBuilt = true + } label: { + Label("Why SimpleX is built.", systemImage: "info.circle") + .font(.headline) } } - .padding(.horizontal, 25) - .padding(.top, 75) - .padding(.bottom, 25) - .frame(minHeight: g.size.height) } + .padding(.horizontal, 25) + .padding(.top, 28) + .padding(.bottom, 20) + .frame(minHeight: g.size.height) .sheet(isPresented: Binding( - get: { m.migrationState != nil }, + get: { m.migrationState != nil && !createProfileNavLinkActive }, set: { _ in m.migrationState = nil MigrationToDeviceState.save(nil) } @@ -86,17 +102,12 @@ struct SimpleXInfo: View { .modifier(ThemedBackground(grouped: true)) } } - .sheet(isPresented: $showHowItWorks) { - HowItWorks( + .sheet(isPresented: $showWhyBuilt) { + WhySimpleX( onboarding: onboarding, createProfileNavLinkActive: $createProfileNavLinkActive ) } - if #available(iOS 16.4, *) { - v.scrollBounceBehavior(.basedOnSize) - } else { - v - } } .onAppear() { setLastVersionDefault() @@ -105,32 +116,12 @@ struct SimpleXInfo: View { .navigationBarHidden(true) // necessary on iOS 15 } - private func onboardingInfoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View { - HStack(alignment: .top) { - Image(image) - .resizable() - .scaledToFit() - .frame(width: width, height: 54) - .frame(width: 54) - .padding(.trailing, 10) - VStack(alignment: .leading, spacing: 4) { - Text(title).font(.headline) - Text(text).frame(minHeight: 40, alignment: .top) - .font(.callout) - .lineLimit(3) - .fixedSize(horizontal: false, vertical: true) - } - .padding(.top, 4) - } - .padding(.bottom, 12) - } - private func createFirstProfileButton() -> some View { ZStack { Button { createProfileNavLinkActive = true } label: { - Text("Create your profile") + Text("Get started") } .buttonStyle(OnboardingButtonStyle(isDisabled: false)) diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 8a7ab465d4..41a342d7c8 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -632,6 +632,38 @@ private let versionDescriptions: [VersionDescription] = [ )) ] ), + VersionDescription( + version: "v6.5", + post: URL(string: "https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html"), + features: [ + .feature(Description( + icon: nil, + title: "Public channels - speak freely 🚀", + description: nil, + subfeatures: [ + ("antenna.radiowaves.left.and.right", "Reliability: many relays per channel."), + ("server.rack", "Ownership: you can run your own relays."), + ("key.2.on.ring", "Security: owners hold channel keys."), + ("person.badge.shield.checkmark", "Privacy: for owners and subscribers."), + ] + )), + .feature(Description( + icon: "link.badge.plus", + title: "Easier to invite your friends 👋", + description: "We made connecting simpler for new users." + )), + .feature(Description( + icon: "network.badge.shield.half.filled", + title: "Safe web links", + description: "- opt-in to send link previews.\n- prevent hyperlink phishing.\n- remove link tracking." + )), + .feature(Description( + icon: "network", + title: "Non-profit governance", + description: "To make SimpleX Network last." + )) + ] + ), ] private let lastVersion = versionDescriptions.last!.version @@ -759,7 +791,7 @@ struct WhatsNewView: View { } } if let post = v.post { - Link(destination: post) { + ExternalLink(destination: post) { HStack { Text("Read more") Image(systemName: "arrow.up.right.circle") diff --git a/apps/ios/Shared/Views/Onboarding/YourNetwork.swift b/apps/ios/Shared/Views/Onboarding/YourNetwork.swift new file mode 100644 index 0000000000..d3727e196e --- /dev/null +++ b/apps/ios/Shared/Views/Onboarding/YourNetwork.swift @@ -0,0 +1,193 @@ +// +// YourNetwork.swift +// SimpleX (iOS) +// +// Created by Evgeny on 22/04/2026. +// Copyright © 2026 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +private enum YourNetworkSheet: Identifiable { + case configureOperators + case configureNotifications + + var id: String { + switch self { + case .configureOperators: return "configureOperators" + case .configureNotifications: return "configureNotifications" + } + } +} + +struct YourNetworkView: View { + @EnvironmentObject var theme: AppTheme + @Environment(\.colorScheme) var colorScheme: ColorScheme + @State private var serverOperators: [ServerOperator] = [] + @State private var selectedOperatorIds = Set() + @State private var notificationMode: NotificationsMode = .instant + @State private var sheetItem: YourNetworkSheet? = nil + @State private var nextStepNavLinkActive = false + @State private var justOpened = true + + var body: some View { + GeometryReader { g in + VStack(alignment: .center, spacing: 10) { + Spacer(minLength: 0) + + #if SIMPLEX_ASSETS + Image(colorScheme == .light ? "your-network" : "your-network-light") + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) + #else + ZStack { + let gp = OnboardingCardView.gradientPoints(aspectRatio: 1.0, scale: colorScheme == .light ? 1.2 : 1.5) + LinearGradient( + stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops, + startPoint: gp.start, + endPoint: gp.end + ) + Image(systemName: "network") + .font(.system(size: 72)) + .foregroundColor(theme.colors.primary) + } + .aspectRatio(1.0, contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 24)) + .padding(.horizontal, 25) + .frame(maxWidth: .infinity) + #endif + + Text("Your network") + .font(.largeTitle) + .bold() + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding(.top, 15) + + Text("Network routers cannot know\nwho talks to whom") + .font(.title3) + .fontWeight(.medium) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + + VStack(alignment: .leading, spacing: 20) { + configureRoutersButton() + configureNotificationsButton() + } + .padding(.top, 15) + .padding(.bottom, 15) + + Spacer(minLength: 0) + + continueButton() + .padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0) + } + .padding(.horizontal, 25) + .padding(.top, 8) + .padding(.bottom, 20) + .frame(minHeight: g.size.height) + } + .onAppear { + if justOpened { + serverOperators = ChatModel.shared.conditions.serverOperators + selectedOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId }) + justOpened = false + } + } + .sheet(item: $sheetItem) { item in + switch item { + case .configureOperators: + ChooseServerOperators(serverOperators: serverOperators, selectedOperatorIds: $selectedOperatorIds) + .modifier(ThemedBackground()) + case .configureNotifications: + SetNotificationsMode(notificationMode: $notificationMode) + .modifier(ThemedBackground()) + } + } + .frame(maxHeight: .infinity) + .navigationBarHidden(true) + } + + private func configureRoutersButton() -> some View { + Button { + sheetItem = .configureOperators + } label: { + HStack(spacing: 6) { + Text("Setup routers") + .fontWeight(.medium) + ForEach(serverOperators.reversed()) { op in + Image(op.logo(colorScheme)) + .resizable() + .scaledToFit() + .frame(width: 22, height: 22) + .grayscale(selectedOperatorIds.contains(op.operatorId) ? 0.0 : 1.0) + } + } + } + } + + private func configureNotificationsButton() -> some View { + Button { + sheetItem = .configureNotifications + } label: { + HStack(spacing: 4) { + Text("Setup notifications") + .fontWeight(.medium) + Image(systemName: notificationMode.icon) + } + } + } + + private func continueButton() -> some View { + ZStack { + Button { + applyNotificationMode() + onboardingStageDefault.set(.step4_NetworkCommitments) + nextStepNavLinkActive = true + } label: { + Text("Continue") + } + .buttonStyle(OnboardingButtonStyle()) + + NavigationLink(isActive: $nextStepNavLinkActive) { + OnboardingConditionsView(selectedOperatorIds: selectedOperatorIds) + .navigationBarBackButtonHidden(true) + .modifier(ThemedBackground()) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + } + + private func applyNotificationMode() { + let m = ChatModel.shared + if let token = m.deviceToken { + switch notificationMode { + case .off: + m.tokenStatus = .new + m.notificationMode = .off + default: + Task { + do { + let status = try await apiRegisterToken(token: token, notificationMode: notificationMode) + await MainActor.run { + m.tokenStatus = status + m.notificationMode = notificationMode + } + } catch let error { + let a = getErrorAlert(error, "Error enabling notifications") + AlertManager.shared.showAlertMsg( + title: a.title, + message: a.message + ) + } + } + } + } + } +} diff --git a/apps/ios/Shared/Views/UserSettings/AppSettings.swift b/apps/ios/Shared/Views/UserSettings/AppSettings.swift index 44e0b20958..8be0798fb1 100644 --- a/apps/ios/Shared/Views/UserSettings/AppSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppSettings.swift @@ -76,7 +76,7 @@ extension AppSettings { c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get() c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get() c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get() - c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) + c.privacyLinkPreviews = privacyLinkPreviewsGroupDefault.get() c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) c.privacySaveLastDraft = def.bool(forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT) c.privacyProtectScreen = def.bool(forKey: DEFAULT_PRIVACY_PROTECT_SCREEN) diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift index 6df2d5422e..a504b00116 100644 --- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift +++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift @@ -22,14 +22,16 @@ struct DeveloperView: View { VStack { List { Section { - ZStack(alignment: .leading) { - Image(colorScheme == .dark ? "github_light" : "github") - .resizable() - .frame(width: 24, height: 24) - .opacity(0.5) - .colorMultiply(theme.colors.secondary) - Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)") - .padding(.leading, 36) + ExternalLink(destination: URL(string: "https://github.com/simplex-chat/simplex-chat")!) { + ZStack(alignment: .leading) { + Image(colorScheme == .dark ? "github_light" : "github") + .resizable() + .frame(width: 24, height: 24) + .opacity(0.5) + .colorMultiply(theme.colors.secondary) + Text("Install SimpleX Chat for terminal") + .padding(.leading, 36) + } } NavigationLink { TerminalView() @@ -91,6 +93,11 @@ struct DeveloperView: View { UserDefaults.standard.set(val, forKey: def) } } + for def in hintGroupDefaults { + if let val = groupAppDefaults[def] as? Bool { + groupDefaults.set(val, forKey: def) + } + } hintsUnchanged = true } } @@ -98,6 +105,8 @@ struct DeveloperView: View { private func hintDefaultsUnchanged() -> Bool { hintDefaults.allSatisfy { def in appDefaults[def] as? Bool == UserDefaults.standard.bool(forKey: def) + } && hintGroupDefaults.allSatisfy { def in + groupAppDefaults[def] as? Bool == groupDefaults.bool(forKey: def) } } diff --git a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift index d9862aaac8..f74516c2c8 100644 --- a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift +++ b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift @@ -23,7 +23,7 @@ struct IncognitoHelp: View { 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("Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).") + ExternalLink("Read more in User Guide.", destination: URL(string: "https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode")!) } .listRowBackground(Color.clear) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift index 6f76e69182..5abbbf8d2e 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift @@ -71,11 +71,7 @@ struct ConditionsWebView: UIViewRepresentable { switch navigationAction.navigationType { case .linkActivated: decisionHandler(.cancel) - if url.absoluteString.starts(with: "https://simplex.chat/contact#") { - ChatModel.shared.appOpenUrl = url - } else { - UIApplication.shared.open(url) - } + openExternalLink(url) default: decisionHandler(.allow) } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index 74b7374654..f10b945dc0 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -332,7 +332,7 @@ struct UsageConditionsView: View { @ViewBuilder private func conditionsDiffButton(_ font: Font? = nil) -> some View { let commit = ChatModel.shared.conditions.currentConditions.conditionsCommit if let commitUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/commit/\(commit)") { - Link(destination: commitUrl) { + ExternalLink(destination: commitUrl) { HStack { Text("Open changes") Image(systemName: "arrow.up.right.circle") @@ -351,21 +351,6 @@ private func regularConditionsHeader() -> some View { } } -struct SimpleConditionsView: View { - - var body: some View { - VStack(alignment: .leading, spacing: 20) { - regularConditionsHeader() - .padding(.top) - .padding(.top) - ConditionsTextView() - .padding(.bottom) - .padding(.bottom) - } - .padding(.horizontal, 25) - .frame(maxHeight: .infinity) - } -} func validateServers_( _ userServers: Binding<[UserOperatorServers]>, diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift index 9d068d3b26..26f24f2f0f 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift @@ -364,11 +364,15 @@ struct OperatorInfoView: View { Text(d) } } - Link(serverOperator.info.website.absoluteString, destination: serverOperator.info.website) + ExternalLink(destination: serverOperator.info.website) { + Text(serverOperator.info.website.absoluteString) + } } if let selfhost = serverOperator.info.selfhost { Section { - Link(selfhost.text, destination: selfhost.link) + ExternalLink(destination: selfhost.link) { + Text(selfhost.text) + } } } } @@ -432,7 +436,7 @@ struct ConditionsTextView: View { private func conditionsLinkView(_ conditionsLink: String) -> some View { VStack(alignment: .leading, spacing: 20) { Text("Current conditions text couldn't be loaded, you can review conditions via this link:") - Link(destination: URL(string: conditionsLink)!) { + ExternalLink(destination: URL(string: conditionsLink)!) { Text(conditionsLink) .multilineTextAlignment(.leading) } @@ -591,11 +595,11 @@ func conditionsLinkButton() -> some View { let commit = ChatModel.shared.conditions.currentConditions.conditionsCommit let mdUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/blob/\(commit)/PRIVACY.md") ?? conditionsURL return Menu { - Link(destination: mdUrl) { + ExternalLink(destination: mdUrl) { Label("Open conditions", systemImage: "doc") } if let commitUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/commit/\(commit)") { - Link(destination: commitUrl) { + ExternalLink(destination: commitUrl) { Label("Open changes", systemImage: "ellipsis") } } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift index e57df4c5dc..b059be7cb0 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift @@ -223,9 +223,7 @@ struct YourServersView: View { func howToButton() -> some View { Button { - DispatchQueue.main.async { - UIApplication.shared.open(howToUrl) - } + openExternalLink(howToUrl) } label: { HStack { Text("How to use your servers") diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index eec820833c..3ae9f0eacd 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -13,7 +13,7 @@ struct PrivacySettings: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true - @AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true + @AppStorage(GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS, store: groupDefaults) private var useLinkPreviews = true @AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = false @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @@ -74,8 +74,8 @@ struct PrivacySettings: View { settingsRow("network", color: theme.colors.secondary) { Toggle("Send link previews", isOn: $useLinkPreviews) .onChange(of: useLinkPreviews) { linkPreviews in - privacyLinkPreviewsGroupDefault.set(linkPreviews) - privacyLinkPreviewsShowAlertGroupDefault.set(false) // to avoid showing alert to current users, show alert in v6.5 + UserDefaults.standard.set(linkPreviews, forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) + privacyLinkPreviewsShowAlertGroupDefault.set(false) } } settingsRow("link", color: theme.colors.secondary) { diff --git a/apps/ios/Shared/Views/UserSettings/RTCServers.swift b/apps/ios/Shared/Views/UserSettings/RTCServers.swift index ef891738cc..b045a8ce55 100644 --- a/apps/ios/Shared/Views/UserSettings/RTCServers.swift +++ b/apps/ios/Shared/Views/UserSettings/RTCServers.swift @@ -139,9 +139,7 @@ struct RTCServers: View { func howToButton() -> some View { Button { - DispatchQueue.main.async { - UIApplication.shared.open(howToUrl) - } + openExternalLink(howToUrl) } label: { HStack{ Text("How to") diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index c091224098..a903329454 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -11,7 +11,7 @@ import SwiftUI import StoreKit import SimpleXChat -let simplexTeamURL = URL(string: "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")! +let simplexTeamURL = URL(string: "simplex:/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw?h=smp6.simplex.im")! let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String @@ -150,6 +150,10 @@ let hintDefaults = [ DEFAULT_SHOW_DELETE_CONTACT_NOTICE ] +let hintGroupDefaults = [ + GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT +] + // not used anymore enum ConnectViaLinkTab: String { case scan @@ -395,7 +399,9 @@ struct SettingsView: View { } Section(header: Text("Support SimpleX Chat").foregroundColor(theme.colors.secondary)) { - settingsRow("keyboard", color: theme.colors.secondary) { Text("[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)") } + settingsRow("keyboard", color: theme.colors.secondary) { + ExternalLink("Contribute", destination: URL(string: "https://github.com/simplex-chat/simplex-chat#contribute")!) + } settingsRow("star", color: theme.colors.secondary) { Button("Rate the app") { if let scene = sceneDelegate.windowScene { @@ -403,14 +409,16 @@ struct SettingsView: View { } } } - ZStack(alignment: .leading) { - Image(colorScheme == .dark ? "github_light" : "github") - .resizable() - .frame(width: 24, height: 24) - .opacity(0.5) - .colorMultiply(theme.colors.secondary) - Text("[Star on GitHub](https://github.com/simplex-chat/simplex-chat)") - .padding(.leading, indent) + ExternalLink(destination: URL(string: "https://github.com/simplex-chat/simplex-chat")!) { + ZStack(alignment: .leading) { + Image(colorScheme == .dark ? "github_light" : "github") + .resizable() + .frame(width: 24, height: 24) + .opacity(0.5) + .colorMultiply(theme.colors.secondary) + Text("Star on GitHub") + .padding(.leading, indent) + } } } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift index 6c1ea8deb2..ac6ae05984 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift @@ -31,7 +31,7 @@ struct UserAddressLearnMore: View { .padding(.top) Text("SimpleX address and 1-time links are safe to share via any messenger.") Text("To protect against your link being replaced, you can compare contact security codes.") - Text("Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).") + ExternalLink("Read more in User Guide.", destination: URL(string: "https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses")!) .padding(.top) } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index d40ec116f4..e22042fa24 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -11,11 +11,13 @@ import MessageUI @preconcurrency import SimpleXChat struct UserAddressView: View { + @Environment(\.colorScheme) var colorScheme @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject private var chatModel: ChatModel @EnvironmentObject var theme: AppTheme @State var shareViaProfile = false @State var autoCreate = false + var onboarding: Bool = false @State private var showShortLink = true @State private var settings = AddressSettingsState() @State private var savedSettings = AddressSettingsState() @@ -54,6 +56,14 @@ struct UserAddressView: View { } } } + .if(onboarding) { v in + v.toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Image(systemName: "info.circle").opacity(0) + } + } + .navigationBarTitleDisplayMode(.inline) + } .onAppear { if chatModel.userAddress == nil, autoCreate { createAddress() @@ -64,12 +74,16 @@ struct UserAddressView: View { private func userAddressView() -> some View { List { if let userAddress = chatModel.userAddress { - existingAddressView(userAddress) - .onAppear { - settings = AddressSettingsState(settings: userAddress.addressSettings) - savedSettings = AddressSettingsState(settings: userAddress.addressSettings) - } - } else { + if onboarding { + onboardingAddressView(userAddress) + } else { + existingAddressView(userAddress) + .onAppear { + settings = AddressSettingsState(settings: userAddress.addressSettings) + savedSettings = AddressSettingsState(settings: userAddress.addressSettings) + } + } + } else if !onboarding { Section { createAddressButton() } header: { @@ -121,8 +135,8 @@ struct UserAddressView: View { ) case .shareOnCreate: return Alert( - title: Text("Share address with contacts?"), - message: Text("Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts."), + title: Text("Share address with SimpleX contacts?"), + message: Text("Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts."), primaryButton: .default(Text("Share")) { setProfileAddress($progressIndicator, true) shareViaProfile = true @@ -157,7 +171,19 @@ struct UserAddressView: View { } addressSettingsButton(userAddress) } header: { + #if SIMPLEX_ASSETS + VStack(alignment: .leading, spacing: 0) { + Image(colorScheme == .light ? "simplex-address-small" : "simplex-address-small-light") + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) + .padding(.top, -20) + ToggleShortLinkHeader(text: Text("For social media"), link: userAddress.connLinkContact, short: $showShortLink) + } + .padding(.bottom, 4) + #else ToggleShortLinkHeader(text: Text("For social media"), link: userAddress.connLinkContact, short: $showShortLink) + #endif } footer: { if settings.businessAddress { Text("Add your team members to the conversations.") @@ -184,6 +210,54 @@ struct UserAddressView: View { } } + @ViewBuilder private func onboardingAddressView(_ userAddress: UserContactLink) -> some View { + Section { + HStack(spacing: 8) { + let link = userAddress.connLinkContact.simplexChatUri(short: showShortLink) + linkTextView(link) + Button { showShareSheet(items: [link]) } label: { + Image(systemName: "square.and.arrow.up") + .padding(.top, -7) + .padding(.horizontal, 8) + } + } + .frame(maxWidth: .infinity) + } header: { + #if SIMPLEX_ASSETS + VStack(alignment: .leading) { + Image(colorScheme == .light ? "simplex-address" : "simplex-address-light") + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) + Text("Use this address in your social media profile, website, or email signature.") + .font(.body).foregroundColor(theme.colors.onBackground).textCase(nil) + } + .padding(.bottom, 4) + #else + Text("Use this address in your social media profile, website, or email signature.") + .font(.body).foregroundColor(theme.colors.onBackground).textCase(nil) + .padding(.bottom, 6) + #endif + } + .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10)) + + Section { + SimpleXCreatedLinkQRCode(link: userAddress.connLinkContact, short: $showShortLink) + .id("simplex-contact-address-qrcode-\(userAddress.connLinkContact.simplexChatUri(short: showShortLink))") + .padding() + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color(uiColor: .secondarySystemGroupedBackground)) + ) + .padding(.horizontal) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + } header: { + Text("Or use this QR - print or show online.").font(.body).foregroundColor(theme.colors.onBackground).textCase(nil) + } + } + private func createAddressButton() -> some View { Button { createAddress() @@ -200,9 +274,22 @@ struct UserAddressView: View { DispatchQueue.main.async { if let connLinkContact { chatModel.userAddress = UserContactLink(connLinkContact) - alert = .shareOnCreate + let hasRelevantContacts = chatModel.chats.contains { chat in + if case let .direct(contact) = chat.chatInfo { + return contact.active && !contact.isContactCard && !contact.contactConnIncognito + } + return false + } + if hasRelevantContacts { + alert = .shareOnCreate + progressIndicator = false + } else { + setProfileAddress($progressIndicator, true) + shareViaProfile = true + } + } else { + progressIndicator = false } - progressIndicator = false } } catch let error { logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))") @@ -486,15 +573,15 @@ struct UserAddressSettingsView: View { private func shareWithContactsButton() -> some View { settingsRow("person", color: theme.colors.secondary) { - Toggle("Share with contacts", isOn: $shareViaProfile) + Toggle("Share with SimpleX contacts", isOn: $shareViaProfile) .onChange(of: shareViaProfile) { on in if ignoreShareViaProfileChange { ignoreShareViaProfileChange = false } else { if on { showAlert( - NSLocalizedString("Share address with contacts?", comment: "alert title"), - message: NSLocalizedString("Profile update will be sent to your contacts.", comment: "alert message"), + NSLocalizedString("Share address with SimpleX contacts?", comment: "alert title"), + message: NSLocalizedString("Profile update will be sent to your SimpleX contacts.", comment: "alert message"), actions: {[ UIAlertAction( title: NSLocalizedString("Cancel", comment: "alert action"), @@ -516,7 +603,7 @@ struct UserAddressSettingsView: View { } else { showAlert( NSLocalizedString("Stop sharing address?", comment: "alert title"), - message: NSLocalizedString("Profile update will be sent to your contacts.", comment: "alert message"), + message: NSLocalizedString("Profile update will be sent to your SimpleX contacts.", comment: "alert message"), actions: {[ UIAlertAction( title: NSLocalizedString("Cancel", comment: "alert action"), diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 569b5caf13..2e609c3f7d 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -26,7 +26,7 @@ struct UserProfile: View { var body: some View { List { - EditProfileImage(profileImage: $profile.image, showChooseSource: $showChooseSource) + EditProfileImage(profileImage: $profile.image, iconName: "person.crop.circle.fill", showChooseSource: $showChooseSource) .padding(.top) Section { @@ -178,6 +178,7 @@ struct EditProfileImage: View { @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner @Binding var profileImage: String? + var iconName: String @Binding var showChooseSource: Bool var body: some View { @@ -193,7 +194,7 @@ struct EditProfileImage: View { } } else { ZStack(alignment: .center) { - ProfileImage(imageStr: profileImage, size: 160) + ProfileImage(imageStr: profileImage, iconName: iconName, size: 160) editImageButton { showChooseSource = true } } } 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 d1bb5fe716..71a7a427be 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -185,6 +185,21 @@ %d месеца time interval + + %d relays failed + channel relay bar +channel subscriber relay bar + + + %d relays not active + channel relay bar +channel subscriber relay bar + + + %d relays removed + channel relay bar +channel subscriber relay bar + %d sec %d сек. @@ -200,11 +215,53 @@ %d пропуснато(и) съобщение(я) integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d седмици time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + channel relay bar + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + channel subscriber relay bar + %lld %lld @@ -215,6 +272,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld избран(и) контакт(а) @@ -315,11 +376,19 @@ %u пропуснати съобщения. No comment provided by engineer. + + (from owner) + chat link info line + (new) (ново) No comment provided by engineer. + + (signed) + chat link info line + (this device v%@) (това устройство v%@) @@ -365,6 +434,10 @@ **Сканирай / Постави линк**: за свързване чрез получения линк. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Внимание**: Незабавните push известия изискват парола, запазена в Keychain. @@ -408,6 +481,12 @@ - и още! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -506,6 +585,10 @@ time interval Още няколко неща No comment provided by engineer. + + A link for one person to connect + No comment provided by engineer. + A new contact Нов контакт @@ -632,9 +715,8 @@ swipe action Активни връзки 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. - Добавете адрес към вашия профил, така че вашите контакти да могат да го споделят с други хора. Актуализацията на профила ще бъде изпратена до вашите контакти. + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. @@ -702,6 +784,10 @@ swipe action Добавени сървъри за съобщения No comment provided by engineer. + + Adding relays will be supported later. + No comment provided by engineer. + Additional accent Допълнителен акцент @@ -821,6 +907,14 @@ swipe action Всички профили profile dropdown + + All relays failed + No comment provided by engineer. + + + All relays removed + No comment provided by engineer. + All reports will be archived for you. Всички доклади за нарушения ще бъдат архивирани за вас. @@ -881,6 +975,10 @@ swipe action Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава. (24 часа) No comment provided by engineer. + + Allow members to chat with admins. + No comment provided by engineer. + Allow message reactions only if your contact allows them. Позволи реакции на съобщения само ако вашият контакт ги разрешава. @@ -896,6 +994,10 @@ swipe action Позволи изпращането на лични съобщения до членовете. No comment provided by engineer. + + Allow sending direct messages to subscribers. + No comment provided by engineer. + Allow sending disappearing messages. Разреши изпращането на изчезващи съобщения. @@ -906,6 +1008,10 @@ swipe action Позволи споделяне No comment provided by engineer. + + Allow subscribers to chat with admins. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Позволи необратимо изтриване на изпратените съобщения. (24 часа) @@ -1011,11 +1117,6 @@ swipe action Отговор на повикване No comment provided by engineer. - - Anybody can host servers. - Протокол и код с отворен код – всеки може да оперира собствени сървъри. - No comment provided by engineer. - App build: %@ Компилация на приложението: %@ @@ -1220,6 +1321,19 @@ swipe action Лош хеш на съобщението No comment provided by engineer. + + Be free +in your network + No comment provided by engineer. + + + Be free in your network. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + No comment provided by engineer. + Better calls По-добри обаждания @@ -1315,6 +1429,10 @@ swipe action Блокирай члена? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Блокиран от админ @@ -1365,6 +1483,14 @@ swipe action И вие, и вашият контакт можете да изпращате гласови съобщения. No comment provided by engineer. + + Bottom bar + No comment provided by engineer. + + + Broadcast + compose placeholder for channel owner + 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)! @@ -1373,7 +1499,7 @@ swipe action Business address Бизнес адрес - No comment provided by engineer. + chat link info line Business chats @@ -1395,15 +1521,6 @@ swipe action Чрез чат профил (по подразбиране) или [чрез връзка](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА). No comment provided by engineer. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - С използването на SimpleX Chat вие се съгласявате със: -- изпращане само на легално съдържание в публични групи. -- уважение към другите потребители – без спам. - No comment provided by engineer. - Call already ended! Разговорът вече приключи! @@ -1552,6 +1669,67 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + alert message +alert subtitle + + + Channel image + No comment provided by engineer. + + + Channel link + chat link info line + + + Channel preferences + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel temporarily unavailable + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + + + Channels + No comment provided by engineer. + Chat Чат @@ -1637,6 +1815,22 @@ set passcode view Потребителски профил No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Тема на чата @@ -1655,7 +1849,8 @@ set passcode view Chat with admins Чат с администраторите - chat toolbar + chat feature +chat toolbar Chat with member @@ -1672,11 +1867,23 @@ set passcode view Чатове No comment provided by engineer. + + Chats with admins are prohibited. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + alert message + Chats with members Чатове с членовете No comment provided by engineer. + + Chats with members are disabled + No comment provided by engineer. + Check messages every 20 min. Проверявай за съобщенията на всеки 20 минути. @@ -1687,6 +1894,14 @@ set passcode view Проверявай за съобщенията, когато е разрешено. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Проверете адреса на сървъра и опитайте отново. @@ -1832,9 +2047,8 @@ set passcode view Конфигурирай ICE сървъри No comment provided by engineer. - - Configure server operators - Конфигуриране на сървърни оператори + + Configure relays No comment provided by engineer. @@ -1895,7 +2109,8 @@ set passcode view Connect Свързване - server test step + relay test step +server test step Connect automatically @@ -1941,6 +2156,10 @@ This is your own one-time link! Свърване чрез линк new chat sheet title + + Connect via link or QR code + No comment provided by engineer. + Connect via one-time link Свързване чрез еднократен линк за връзка @@ -2019,7 +2238,7 @@ This is your own one-time link! Connection error (AUTH) Грешка при свързване (AUTH) - No comment provided by engineer. + conn error description Connection failed @@ -2074,6 +2293,10 @@ This is your own one-time link! Connections No comment provided by engineer. + + Contact address + chat link info line + Contact allows Контактът позволява @@ -2139,6 +2362,11 @@ This is your own one-time link! Продължи No comment provided by engineer. + + Contribute + Допринеси + No comment provided by engineer. + Conversation deleted! No comment provided by engineer. @@ -2164,12 +2392,7 @@ This is your own one-time link! Correct name to %@? Поправи име на %@? - No comment provided by engineer. - - - Create - Създаване - No comment provided by engineer. + alert message Create 1-time link @@ -2220,6 +2443,14 @@ This is your own one-time link! Създай профил No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Създай опашка @@ -2229,11 +2460,19 @@ This is your own one-time link! Create your address No comment provided by engineer. + + Create your link + No comment provided by engineer. + Create your profile Създай своя профил No comment provided by engineer. + + Create your public address + No comment provided by engineer. + Created No comment provided by engineer. @@ -2253,6 +2492,10 @@ This is your own one-time link! Създаване на архивен линк No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Линкът се създава… @@ -2405,10 +2648,9 @@ This is your own one-time link! Debug delivery No comment provided by engineer. - - Decentralized - Децентрализиран - No comment provided by engineer. + + Decode link + relay test step Decryption error @@ -2455,6 +2697,14 @@ swipe action Изтрий и уведоми контакт No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat No comment provided by engineer. @@ -2617,6 +2867,10 @@ alert button Изтрий опашка server test step + + Delete relay + No comment provided by engineer. + Delete report No comment provided by engineer. @@ -2767,6 +3021,14 @@ alert button Личните съобщения между членовете са забранени в тази група. No comment provided by engineer. + + Direct messages between subscribers are prohibited. + No comment provided by engineer. + + + Disable + alert button + Disable (keep overrides) Деактивиране (запазване на промените) @@ -2867,6 +3129,10 @@ alert button Не изпращай история на нови членове. No comment provided by engineer. + + Do not send history to new subscribers. + No comment provided by engineer. + Do not use credentials with proxy. No comment provided by engineer. @@ -2959,11 +3225,19 @@ chat item action E2E encrypted notifications. No comment provided by engineer. + + Easier to invite your friends 👋 + No comment provided by engineer. + Edit Редактирай chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Редактирай групов профил @@ -2976,7 +3250,7 @@ chat item action Enable Активирай - No comment provided by engineer. + alert button Enable (keep overrides) @@ -2997,6 +3271,10 @@ chat item action Активирай TCP keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Активиране на автоматично изтриване на съобщения? @@ -3007,6 +3285,10 @@ chat item action Разреши достъпа до камерата No comment provided by engineer. + + Enable chats with admins? + alert title + Enable disappearing messages by default. No comment provided by engineer. @@ -3026,16 +3308,15 @@ chat item action Активирай незабавни известия? No comment provided by engineer. + + Enable link previews? + alert title + Enable lock Активирай заключване No comment provided by engineer. - - Enable notifications - Активирай известията - No comment provided by engineer. - Enable periodic notifications? Активирай периодични известия? @@ -3139,6 +3420,10 @@ chat item action Въведете kодa за достъп No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Въведи правилна парола. @@ -3164,6 +3449,14 @@ chat item action Въведете парола по-горе, за да се покаже! No comment provided by engineer. + + Enter profile name... + No comment provided by engineer. + + + Enter relay name… + No comment provided by engineer. + Enter server manually Въведи сървъра ръчно @@ -3192,7 +3485,7 @@ chat item action Error Грешка при свързване със сървъра - No comment provided by engineer. + conn error description Error aborting address change @@ -3217,6 +3510,10 @@ chat item action Грешка при добавяне на член(ове) No comment provided by engineer. + + Error adding relay + alert title + Error adding server alert title @@ -3269,6 +3566,10 @@ chat item action Грешка при създаване на адрес No comment provided by engineer. + + Error creating channel + alert title + Error creating group Грешка при създаване на група @@ -3398,10 +3699,6 @@ chat item action Грешка при отваряне на чата No comment provided by engineer. - - Error opening group - No comment provided by engineer. - Error receiving file Грешка при получаване на файл @@ -3441,6 +3738,10 @@ chat item action Грешка при запазване на ICE сървърите No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list alert title @@ -3503,6 +3804,10 @@ chat item action Грешка при настройването на потвърждениeто за доставка!! No comment provided by engineer. + + Error sharing channel + alert title + Error starting chat Грешка при стартиране на чата @@ -3579,7 +3884,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3802,7 +4108,8 @@ snd error text Fingerprint in server address does not match certificate. Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3842,9 +4149,14 @@ snd error text For all moderators No comment provided by engineer. + + For anyone to reach you + No comment provided by engineer. + For chat profile %@: - servers error + servers error +servers warning For console @@ -3969,10 +4281,18 @@ Error: %2$@ GIF файлове и стикери No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. No comment provided by engineer. + + Get started + No comment provided by engineer. + Good afternoon! message preview @@ -4029,7 +4349,7 @@ Error: %2$@ Group link Групов линк - No comment provided by engineer. + chat link info line Group links @@ -4138,6 +4458,10 @@ Error: %2$@ Историята не се изпраща на нови членове. No comment provided by engineer. + + History is not sent to new subscribers. + No comment provided by engineer. + How SimpleX works Как работи SimpleX @@ -4232,11 +4556,6 @@ Error: %2$@ Веднага No comment provided by engineer. - - Immune to spam - Защитен от спам и злоупотреби - No comment provided by engineer. - Import Импортиране @@ -4374,9 +4693,9 @@ More improvements are coming soon! Първоначална роля 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) + + Install SimpleX Chat for terminal + Инсталирайте SimpleX Chat за терминал No comment provided by engineer. @@ -4428,7 +4747,7 @@ More improvements are coming soon! Invalid connection link Невалиден линк за връзка - No comment provided by engineer. + conn error description Invalid display name! @@ -4448,7 +4767,15 @@ More improvements are coming soon! Invalid name! Невалидно име! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4484,6 +4811,10 @@ More improvements are coming soon! Покани членове No comment provided by engineer. + + Invite someone privately + No comment provided by engineer. + Invite to chat No comment provided by engineer. @@ -4558,6 +4889,10 @@ More improvements are coming soon! присъединяване като %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Влез в групата @@ -4643,6 +4978,14 @@ This is your link for group %@! Напусни swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat No comment provided by engineer. @@ -4665,6 +5008,10 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let someone connect to you + No comment provided by engineer. + Let's talk in SimpleX Chat Нека да поговорим в SimpleX Chat @@ -4685,6 +5032,10 @@ This is your link for group %@! Свържете мобилни и настолни приложения! 🔗 No comment provided by engineer. + + Link signature verified. + owner verification + Linked desktop options Настройки на запомнени настолни устройства @@ -4854,6 +5205,10 @@ This is your link for group %@! Членовете на групата могат да добавят реакции към съобщенията. No comment provided by engineer. + + Members can chat with admins. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа) @@ -4915,6 +5270,10 @@ This is your link for group %@! Чернова на съобщение No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded item status text @@ -5000,6 +5359,14 @@ This is your link for group %@! Съобщенията от %@ ще бъдат показани! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + E2EE info chat item + Messages in this chat will never be deleted. alert message @@ -5026,16 +5393,15 @@ This is your link for group %@! Съобщенията, файловете и разговорите са защитени чрез **квантово устойчиво e2e криптиране** с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом. No comment provided by engineer. + + Migrate + No comment provided by engineer. + Migrate device Мигрирай устройството No comment provided by engineer. - - Migrate from another device - Мигриране от друго устройство - No comment provided by engineer. - Migrate here Мигрирай тук @@ -5153,6 +5519,10 @@ This is your link for group %@! Мрежа и сървъри No comment provided by engineer. + + Network commitments + No comment provided by engineer. + Network connection Мрежова връзка @@ -5162,6 +5532,10 @@ This is your link for group %@! Network decentralization No comment provided by engineer. + + Network error + conn error description + Network issues - message expired after many attempts to send it. snd error text @@ -5175,6 +5549,11 @@ This is your link for group %@! Network operator No comment provided by engineer. + + Network routers cannot know +who talks to whom + No comment provided by engineer. + Network settings Мрежови настройки @@ -5189,6 +5568,10 @@ This is your link for group %@! New token status text + + New 1-time link + No comment provided by engineer. + New Passcode Нов kод за достъп @@ -5211,6 +5594,10 @@ This is your link for group %@! New chat experience 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Нова заявка за контакт @@ -5276,11 +5663,28 @@ This is your link for group %@! Не No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + No comment provided by engineer. + + + No active relays + No comment provided by engineer. + No app password Приложението няма kод за достъп Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats No comment provided by engineer. @@ -5408,11 +5812,22 @@ This is your link for group %@! No unread chats No comment provided by engineer. - - No user identifiers. - Първата платформа без никакви потребителски идентификатори – поверителна по дизайн. + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. + + Non-profit governance + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + No comment provided by engineer. + + + Not all relays connected + alert title + Not compatible! Несъвместим! @@ -5464,7 +5879,7 @@ This is your link for group %@! OK ОК - No comment provided by engineer. + alert button Off @@ -5483,11 +5898,19 @@ new chat action Стара база данни No comment provided by engineer. + + On your phone, not on servers. + No comment provided by engineer. + One-time invitation link Линк за еднократна покана No comment provided by engineer. + + One-time link + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5507,6 +5930,10 @@ Requires compatible VPN. Няма се използват Onion хостове. No comment provided by engineer. + + Only channel owners can change channel preferences. + No comment provided by engineer. + Only chat owners can change preferences. No comment provided by engineer. @@ -5604,7 +6031,8 @@ Requires compatible VPN. Open Отвори - alert action + alert action +alert button Open Settings @@ -5615,6 +6043,10 @@ Requires compatible VPN. Open changes No comment provided by engineer. + + Open channel + new chat action + Open chat Отвори чат @@ -5633,6 +6065,10 @@ Requires compatible VPN. Open conditions No comment provided by engineer. + + Open external link? + alert title + Open full link alert action @@ -5651,6 +6087,10 @@ Requires compatible VPN. Отвори миграцията към друго устройство authentication reason + + Open new channel + new chat action + Open new chat new chat action @@ -5688,6 +6128,13 @@ Requires compatible VPN. Operator server alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + No comment provided by engineer. + Or import archive file No comment provided by engineer. @@ -5707,6 +6154,10 @@ Requires compatible VPN. Или сигурно споделете този линк към файла No comment provided by engineer. + + Or show QR in person or via video call. + No comment provided by engineer. + Or show this code Или покажи този код @@ -5716,6 +6167,10 @@ Requires compatible VPN. Or to share privately No comment provided by engineer. + + Or use this QR - print or show online. + No comment provided by engineer. + Organize chats into lists No comment provided by engineer. @@ -5730,6 +6185,18 @@ Requires compatible VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + + + Ownership: you can run your own relays. + No comment provided by engineer. + PING count PING бройка @@ -5784,6 +6251,10 @@ Requires compatible VPN. Постави изображение No comment provided by engineer. + + Paste link / Scan + No comment provided by engineer. + Paste link to connect! Поставете линк, за да се свържете! @@ -5928,6 +6399,14 @@ Error: %@ Запазете последната чернова на съобщението с прикачени файлове. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Предварително зададен адрес на сървъра @@ -5959,13 +6438,12 @@ Error: %@ Privacy policy and conditions of use. No comment provided by engineer. - - Privacy redefined - Поверителността преосмислена + + Privacy: for owners and subscribers. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. + + Private and secure messaging. No comment provided by engineer. @@ -6002,6 +6480,10 @@ Error: %@ Private routing timeout alert title + + Proceed + alert action + Profile and server connections Профилни и сървърни връзки @@ -6026,9 +6508,8 @@ Error: %@ Profile theme No comment provided by engineer. - - Profile update will be sent to your contacts. - Актуализацията на профила ще бъде изпратена до вашите контакти. + + Profile update will be sent to your SimpleX contacts. alert message @@ -6036,6 +6517,10 @@ Error: %@ Забрани аудио/видео разговорите. No comment provided by engineer. + + Prohibit chats with admins. + No comment provided by engineer. + Prohibit irreversible message deletion. Забрани необратимото изтриване на съобщения. @@ -6065,6 +6550,10 @@ Error: %@ Забрани изпращането на лични съобщения до членовете. No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + No comment provided by engineer. + Prohibit sending disappearing messages. Забрани изпращането на изчезващи съобщения. @@ -6125,6 +6614,10 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. + + Public channels - speak freely 🚀 + No comment provided by engineer. + Push notifications Push известия @@ -6164,24 +6657,14 @@ Enable in *Network & servers* settings. Прочетете още No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + Прочетете повече в Ръководство за потребителя. No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - 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). + + Read more in our GitHub repository. + Прочетете повече в нашето GitHub хранилище. No comment provided by engineer. @@ -6328,6 +6811,26 @@ swipe action Reject member? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + + + Relay results: + alert message + Relay server is only used if necessary. Another party can observe your IP address. Реле сървър се използва само ако е необходимо. Друга страна може да наблюдава вашия IP адрес. @@ -6338,6 +6841,14 @@ swipe action Relay сървърът защитава вашия IP адрес, но може да наблюдава продължителността на разговора. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + + + Reliability: many relays per channel. + No comment provided by engineer. + Remove Премахване @@ -6374,6 +6885,14 @@ swipe action Премахване на паролата от keychain? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. No comment provided by engineer. @@ -6586,6 +7105,10 @@ swipe action SOCKS proxy No comment provided by engineer. + + Safe web links + No comment provided by engineer. + Safely receive files No comment provided by engineer. @@ -6610,6 +7133,10 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + alert button + Save admission settings? alert title @@ -6624,6 +7151,10 @@ chat item action Запази и уведоми членовете на групата No comment provided by engineer. + + Save and notify subscribers + No comment provided by engineer. + Save and reconnect No comment provided by engineer. @@ -6633,6 +7164,14 @@ chat item action Запази и актуализирай профила на групата No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Запази профила на групата @@ -6800,6 +7339,10 @@ chat item action Код за сигурност No comment provided by engineer. + + Security: owners hold channel keys. + No comment provided by engineer. + Select Избери @@ -6919,6 +7462,10 @@ chat item action Send request without message No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. Изпрати от галерия или персонализирани клавиатури. @@ -6929,6 +7476,10 @@ chat item action Изпращане до последните 100 съобщения на нови членове. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + No comment provided by engineer. + Send your private feedback to groups. No comment provided by engineer. @@ -6943,6 +7494,10 @@ chat item action Подателят може да е изтрил заявката за връзка. No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти във всички видими чат профили. @@ -7055,6 +7610,10 @@ chat item action Server protocol changed. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Сървърът изисква оторизация за създаване на опашки, проверете паролата @@ -7174,6 +7733,14 @@ chat item action Settings were changed. alert message + + Setup notifications + No comment provided by engineer. + + + Setup routers + No comment provided by engineer. + Shape profile images Променете формата на профилните изображения @@ -7207,11 +7774,14 @@ chat item action Share address publicly No comment provided by engineer. - - Share address with contacts? - Сподели адреса с контактите? + + Share address with SimpleX contacts? alert title + + Share channel + No comment provided by engineer. + Share from other apps. No comment provided by engineer. @@ -7233,6 +7803,10 @@ chat item action Share profile No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Сподели този еднократен линк за връзка @@ -7242,9 +7816,12 @@ chat item action Share to SimpleX No comment provided by engineer. - - Share with contacts - Сподели с контактите + + Share via chat + No comment provided by engineer. + + + Share with SimpleX contacts No comment provided by engineer. @@ -7404,8 +7981,8 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7473,6 +8050,11 @@ report reason Квадрат, кръг или нещо между тях. No comment provided by engineer. + + Star on GitHub + Звезда в GitHub + No comment provided by engineer. + Start chat Започни чат @@ -7568,6 +8150,63 @@ report reason Subscribed No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber reports + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers can add message reactions. + No comment provided by engineer. + + + Subscribers can chat with admins. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + No comment provided by engineer. + + + Subscribers can send direct messages. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + No comment provided by engineer. + + + Subscribers can send files and media. + No comment provided by engineer. + + + Subscribers can send voice messages. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors No comment provided by engineer. @@ -7640,6 +8279,10 @@ report reason Направи снимка No comment provided by engineer. + + Talk to someone + No comment provided by engineer. + Tap Connect to chat No comment provided by engineer. @@ -7652,8 +8295,8 @@ report reason Tap Connect to use bot No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. + + Tap Join channel No comment provided by engineer. @@ -7685,6 +8328,10 @@ report reason Докосни за инкогнито вход No comment provided by engineer. + + Tap to open + No comment provided by engineer. + Tap to paste link Докосни за поставяне на линк за връзка @@ -7702,12 +8349,17 @@ report reason Test failed at step %@. Тестът е неуспешен на стъпка %@. - server test failure + relay test failure +server test failure Test notifications No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Тествай сървър @@ -7758,6 +8410,10 @@ It can happen because of some bug or when the connection is compromised.The app protects your privacy by using different operators in each conversation. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -7772,6 +8428,10 @@ It can happen because of some bug or when the connection is compromised.QR кодът, който сканирахте, не е SimpleX линк за връзка. No comment provided by engineer. + + The connection reached the limit of undelivered messages + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. No comment provided by engineer. @@ -7796,9 +8456,9 @@ It can happen because of some bug or when the connection is compromised.Криптирането работи и новото споразумение за криптиране не е необходимо. Това може да доведе до грешки при свързване! No comment provided by engineer. - - The future of messaging - Ново поколение поверителни съобщения + + The first network where you own +your contacts and groups. No comment provided by engineer. @@ -7833,6 +8493,10 @@ It can happen because of some bug or when the connection is compromised.Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита. No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + No comment provided by engineer. + The same conditions will apply to operator **%@**. No comment provided by engineer. @@ -7873,6 +8537,14 @@ It can happen because of some bug or when the connection is compromised.Themes No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + No comment provided by engineer. + These conditions will also apply for: **%@**. No comment provided by engineer. @@ -7936,6 +8608,14 @@ It can happen because of some bug or when the connection is compromised.Тази група вече не съществува. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -7980,6 +8660,10 @@ It can happen because of some bug or when the connection is compromised.Скриване на нежелани съобщения. No comment provided by engineer. + + To make SimpleX Network last. + No comment provided by engineer. + To make a new connection За да направите нова връзка @@ -8058,10 +8742,6 @@ You will be prompted to complete authentication before this feature is enabled.< За да проверите криптирането от край до край с вашия контакт, сравнете (или сканирайте) кода на вашите устройства. No comment provided by engineer. - - Toggle chat list: - No comment provided by engineer. - Toggle incognito when connecting. Избор на инкогнито при свързване. @@ -8075,6 +8755,10 @@ You will be prompted to complete authentication before this feature is enabled.< Toolbar opacity No comment provided by engineer. + + Top bar + No comment provided by engineer. + Total No comment provided by engineer. @@ -8137,6 +8821,10 @@ You will be prompted to complete authentication before this feature is enabled.< Отблокирай член? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -8234,13 +8922,17 @@ To connect, please ask your contact to create another connection link and check Unsupported connection link - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. На новите членове се изпращат до последните 100 съобщения. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + No comment provided by engineer. + Update Актуализация @@ -8351,11 +9043,6 @@ To connect, please ask your contact to create another connection link and check Use TCP port 443 for preset servers only. No comment provided by engineer. - - Use chat - Използвай чата - No comment provided by engineer. - Use current profile Използвай текущия профил @@ -8369,6 +9056,10 @@ To connect, please ask your contact to create another connection link and check Use for messages No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Използвай за нови връзки @@ -8406,6 +9097,10 @@ To connect, please ask your contact to create another connection link and check Use private routing with unknown servers. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Използвай сървър @@ -8424,6 +9119,10 @@ To connect, please ask your contact to create another connection link and check Use the app with one hand. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + No comment provided by engineer. + Use web port No comment provided by engineer. @@ -8441,6 +9140,10 @@ To connect, please ask your contact to create another connection link and check Използват се сървърите на SimpleX Chat. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Потвърди кода с настолното устройство @@ -8558,6 +9261,18 @@ To connect, please ask your contact to create another connection link and check Гласово съобщение… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + + + Waiting for channel owner to add relays. + No comment provided by engineer. + Waiting for desktop... Изчакване на настолно устройство… @@ -8596,6 +9311,10 @@ To connect, please ask your contact to create another connection link and check Предупреждение: Може да загубите някои данни! No comment provided by engineer. + + We made connecting simpler for new users. + No comment provided by engineer. + WebRTC ICE servers WebRTC ICE сървъри @@ -8644,6 +9363,10 @@ To connect, please ask your contact to create another connection link and check Когато споделяте инкогнито профил с някого, този профил ще се използва за групите, в които той ви кани. No comment provided by engineer. + + Why SimpleX is built. + No comment provided by engineer. + WiFi WiFi @@ -8843,6 +9566,10 @@ Repeat join request? Можете да зададете визуализация на известията на заключен екран през настройките. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + 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 код - всеки ще може да се присъедини към групата. Няма да загубите членовете на групата, ако по-късно я изтриете. @@ -8886,16 +9613,21 @@ Repeat join request? Не може да изпращате съобщения! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Не можахте да бъдете потвърдени; Моля, опитайте отново. No comment provided by engineer. - - You decide who can connect. - Хората могат да се свържат с вас само чрез ликовете, които споделяте. - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -8959,6 +9691,10 @@ Repeat connection request? You should receive notifications. token info + + You were born without an account + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. No comment provided by engineer. @@ -8993,6 +9729,10 @@ Repeat connection request? Все още ще получавате обаждания и известия от заглушени профили, когато са активни. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. No comment provided by engineer. @@ -9036,6 +9776,10 @@ Repeat connection request? Вашите обаждания No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Вашата база данни @@ -9082,6 +9826,10 @@ Repeat connection request? Вашите контакти ще останат свързани. No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + No comment provided by engineer. + Your credentials may be sent unencrypted. No comment provided by engineer. @@ -9100,6 +9848,10 @@ Repeat connection request? Your group No comment provided by engineer. + + Your network + No comment provided by engineer. + Your preferences Вашите настройки @@ -9115,6 +9867,11 @@ Repeat connection request? Вашият профил No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Вашият профил **%@** ще бъде споделен. @@ -9134,11 +9891,23 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. alert message + + Your public address + No comment provided by engineer. + Your random profile Вашият автоматично генериран профил No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Вашият адрес на сървъра @@ -9153,21 +9922,11 @@ Repeat connection request? Вашите настройки 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. - \_italic_ \_курсив_ @@ -9183,6 +9942,10 @@ Repeat connection request? по-горе, след това избери: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ rcv group event chat item @@ -9200,6 +9963,10 @@ Repeat connection request? accepted you rcv group event chat item + + active + No comment provided by engineer. + admin админ @@ -9307,6 +10074,10 @@ marked deleted chat item preview text повикване… call status + + can't broadcast + No comment provided by engineer. + can't send messages No comment provided by engineer. @@ -9341,6 +10112,14 @@ marked deleted chat item preview text промяна на адреса… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored цветен @@ -9482,6 +10261,10 @@ pref value изтрит deleted chat item + + deleted channel + rcv group event chat item + deleted contact изтрит контакт @@ -9591,6 +10374,10 @@ pref value грешка No comment provided by engineer. + + error: %@ + receive error chat item + expired No comment provided by engineer. @@ -9715,6 +10502,10 @@ pref value напусна rcv group event chat item + + link + No comment provided by engineer. + marked deleted маркирано като изтрито @@ -9782,6 +10573,10 @@ pref value никога delete after time + + new + No comment provided by engineer. + new message ново съобщение @@ -9897,6 +10692,10 @@ time to disappear отхвърлено повикване call status + + relay + member role + removed отстранен @@ -9907,6 +10706,14 @@ time to disappear отстранен %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + + + removed by operator + No comment provided by engineer. + removed contact address премахнат адрес за контакт @@ -10047,6 +10854,10 @@ last received msg: %2$@ unprotected No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile актуализиран профил на групата @@ -10067,6 +10878,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link чрез линк с адрес за контакт @@ -10139,6 +10954,10 @@ last received msg: %2$@ вие сте наблюдател No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ вие блокирахте %@ @@ -10199,6 +11018,10 @@ last received msg: %2$@ \~зачеркнат~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + owner verification + 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 b212f42592..1cc44dd7cb 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -185,6 +185,21 @@ %d měsíce time interval + + %d relays failed + channel relay bar +channel subscriber relay bar + + + %d relays not active + channel relay bar +channel subscriber relay bar + + + %d relays removed + channel relay bar +channel subscriber relay bar + %d sec %d sek @@ -200,11 +215,53 @@ %d přeskočené zprávy integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d týdnů time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + channel relay bar + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + channel subscriber relay bar + %lld %lld @@ -215,6 +272,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld kontakt(y) vybrané @@ -227,6 +288,7 @@ %lld group events + %lld událostí skupiny No comment provided by engineer. @@ -314,11 +376,19 @@ %u zpráv přeskočeno. No comment provided by engineer. + + (from owner) + chat link info line + (new) (nový) No comment provided by engineer. + + (signed) + chat link info line + (this device v%@) (toto zařízení v%@) @@ -363,6 +433,10 @@ **Skenovat / Vložit odkaz**: pro připojení pomocí odkazu který jste obdrželi. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Upozornění**: Okamžitě doručovaná oznámení vyžadují přístupové heslo uložené v Klíčence. @@ -406,6 +480,12 @@ - a více! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -503,6 +583,10 @@ time interval Ještě pár věcí No comment provided by engineer. + + A link for one person to connect + No comment provided by engineer. + A new contact Nový kontakt @@ -515,7 +599,7 @@ time interval 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**. + Samostatné připojení TCP bude použito **pro každý profil chatu, který máte v aplikaci**. No comment provided by engineer. @@ -626,9 +710,8 @@ swipe action Aktivní spojení 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. - Přidejte adresu do svého profilu, aby ji vaše kontakty mohly sdílet s dalšími lidmi. Aktualizace profilu bude zaslána vašim kontaktům. + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. @@ -693,6 +776,10 @@ swipe action Přidané servery zpráv No comment provided by engineer. + + Adding relays will be supported later. + No comment provided by engineer. + Additional accent Další zbarvení @@ -764,7 +851,7 @@ swipe action All chats will be removed from the list %@, and the list deleted. - Všechny chaty budou odstraněny ze seznamu %@ a seznam bude odstraněn. + Všechny chaty budou odstraněny ze seznamu %@ a seznam bude smazán. alert message @@ -809,6 +896,14 @@ swipe action Všechny profily profile dropdown + + All relays failed + No comment provided by engineer. + + + All relays removed + No comment provided by engineer. + All reports will be archived for you. No comment provided by engineer. @@ -865,6 +960,10 @@ swipe action Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí. (24 hodin) No comment provided by engineer. + + Allow members to chat with admins. + No comment provided by engineer. + Allow message reactions only if your contact allows them. Povolit reakce na zprávy, pokud je váš kontakt povolí. @@ -880,6 +979,10 @@ swipe action Povolit odesílání přímých zpráv členům. No comment provided by engineer. + + Allow sending direct messages to subscribers. + No comment provided by engineer. + Allow sending disappearing messages. Povolit odesílání mizících zpráv. @@ -890,6 +993,10 @@ swipe action Povolit sdílení No comment provided by engineer. + + Allow subscribers to chat with admins. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Povolit nevratné smazání odeslaných zpráv. (24 hodin) @@ -982,7 +1089,7 @@ swipe action An empty chat profile with the provided name is created, and the app opens as usual. - Vytvořit prázdný chat profil se zadaným názvem a otevřít aplikaci jako obvykle. + Vytvořit prázdný profil chatu se zadaným názvem a otevřít aplikaci jako obvykle. No comment provided by engineer. @@ -995,11 +1102,6 @@ swipe action Přijmout hovor No comment provided by engineer. - - Anybody can host servers. - Servery může provozovat kdokoli. - No comment provided by engineer. - App build: %@ Sestavení aplikace: %@ @@ -1194,6 +1296,21 @@ swipe action Špatný hash zprávy No comment provided by engineer. + + Be free +in your network + No comment provided by engineer. + + + Be free in your network. + Buďte svobodní ve své síti. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + Protože jsme zničili sílu vědět, kdo jste. Aby vám vaši moc nikdo nemohl vzít. + No comment provided by engineer. + Better calls Lepší volání @@ -1281,6 +1398,10 @@ swipe action Blokovat člena? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Blokován správcem @@ -1329,6 +1450,14 @@ swipe action Hlasové zprávy můžete posílat vy i váš kontakt. No comment provided by engineer. + + Bottom bar + No comment provided by engineer. + + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Bulharský, finský, thajský a ukrajinský - díky uživatelům a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1337,7 +1466,7 @@ swipe action Business address Obchodní adresa - No comment provided by engineer. + chat link info line Business chats @@ -1353,13 +1482,7 @@ swipe action By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). - 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). - No comment provided by engineer. - - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. + Podle profilu chatu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. @@ -1461,7 +1584,7 @@ new chat action Change chat profiles - Změnit chat profily + Změnit profily chatu authentication reason @@ -1510,6 +1633,67 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + alert message +alert subtitle + + + Channel image + No comment provided by engineer. + + + Channel link + chat link info line + + + Channel preferences + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel temporarily unavailable + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + + + Channels + No comment provided by engineer. + Chat No comment provided by engineer. @@ -1589,6 +1773,22 @@ set passcode view Profil uživatele No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme No comment provided by engineer. @@ -1603,7 +1803,8 @@ set passcode view Chat with admins - chat toolbar + chat feature +chat toolbar Chat with member @@ -1618,10 +1819,22 @@ set passcode view Chaty No comment provided by engineer. + + Chats with admins are prohibited. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + alert message + Chats with members No comment provided by engineer. + + Chats with members are disabled + No comment provided by engineer. + Check messages every 20 min. No comment provided by engineer. @@ -1630,6 +1843,14 @@ set passcode view Check messages when allowed. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Zkontrolujte adresu serveru a zkuste to znovu. @@ -1757,8 +1978,8 @@ set passcode view Konfigurace serverů ICE No comment provided by engineer. - - Configure server operators + + Configure relays No comment provided by engineer. @@ -1814,7 +2035,8 @@ set passcode view Connect Připojit - server test step + relay test step +server test step Connect automatically @@ -1854,6 +2076,10 @@ Toto je váš vlastní jednorázový odkaz! Připojte se prostřednictvím odkazu new chat sheet title + + Connect via link or QR code + No comment provided by engineer. + Connect via one-time link Připojit se jednorázovým odkazem @@ -1922,7 +2148,7 @@ Toto je váš vlastní jednorázový odkaz! Connection error (AUTH) Chyba spojení (AUTH) - No comment provided by engineer. + conn error description Connection failed @@ -1971,6 +2197,10 @@ Toto je váš vlastní jednorázový odkaz! Connections No comment provided by engineer. + + Contact address + chat link info line + Contact allows Kontakt povolil @@ -2036,6 +2266,11 @@ Toto je váš vlastní jednorázový odkaz! Pokračovat No comment provided by engineer. + + Contribute + Přispějte + No comment provided by engineer. + Conversation deleted! No comment provided by engineer. @@ -2060,12 +2295,7 @@ Toto je váš vlastní jednorázový odkaz! Correct name to %@? - No comment provided by engineer. - - - Create - Vytvořit - No comment provided by engineer. + alert message Create 1-time link @@ -2113,6 +2343,14 @@ Toto je váš vlastní jednorázový odkaz! Vytvořte si profil No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Vytvořit frontu @@ -2122,11 +2360,19 @@ Toto je váš vlastní jednorázový odkaz! Create your address No comment provided by engineer. + + Create your link + No comment provided by engineer. + Create your profile Vytvořte si profil No comment provided by engineer. + + Create your public address + No comment provided by engineer. + Created No comment provided by engineer. @@ -2143,6 +2389,10 @@ Toto je váš vlastní jednorázový odkaz! Creating archive link No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… No comment provided by engineer. @@ -2294,10 +2544,9 @@ Toto je váš vlastní jednorázový odkaz! Debug delivery No comment provided by engineer. - - Decentralized - Decentralizované - No comment provided by engineer. + + Decode link + relay test step Decryption error @@ -2342,6 +2591,14 @@ swipe action Delete and notify contact No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat No comment provided by engineer. @@ -2352,12 +2609,12 @@ swipe action Delete chat profile - Smazat chat profil + Smazat profil chatu No comment provided by engineer. Delete chat profile? - Smazat chat profil? + Smazat profil chatu? No comment provided by engineer. @@ -2503,6 +2760,10 @@ alert button Odstranit frontu server test step + + Delete relay + No comment provided by engineer. + Delete report No comment provided by engineer. @@ -2650,6 +2911,14 @@ alert button Přímé zprávy mezi členy jsou v této skupině zakázány. No comment provided by engineer. + + Direct messages between subscribers are prohibited. + No comment provided by engineer. + + + Disable + alert button + Disable (keep overrides) Vypnout (zachovat přepsání) @@ -2747,6 +3016,10 @@ alert button Do not send history to new members. No comment provided by engineer. + + Do not send history to new subscribers. + No comment provided by engineer. + Do not use credentials with proxy. No comment provided by engineer. @@ -2835,11 +3108,19 @@ chat item action E2E encrypted notifications. No comment provided by engineer. + + Easier to invite your friends 👋 + No comment provided by engineer. + Edit Upravit chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Upravit profil skupiny @@ -2852,7 +3133,7 @@ chat item action Enable Zapnout - No comment provided by engineer. + alert button Enable (keep overrides) @@ -2873,6 +3154,10 @@ chat item action Povolit TCP keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Povolit automatické mazání zpráv? @@ -2882,6 +3167,10 @@ chat item action Enable camera access No comment provided by engineer. + + Enable chats with admins? + alert title + Enable disappearing messages by default. No comment provided by engineer. @@ -2900,16 +3189,15 @@ chat item action Povolit okamžitá oznámení? No comment provided by engineer. + + Enable link previews? + alert title + Enable lock Povolit zámek No comment provided by engineer. - - Enable notifications - Povolit upozornění - No comment provided by engineer. - Enable periodic notifications? Povolit pravidelná oznámení? @@ -3009,6 +3297,10 @@ chat item action Zadat heslo No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Zadejte správnou přístupovou frázi. @@ -3032,6 +3324,14 @@ chat item action Zadejte heslo do hledání! No comment provided by engineer. + + Enter profile name... + No comment provided by engineer. + + + Enter relay name… + No comment provided by engineer. + Enter server manually Zadejte server ručně @@ -3058,7 +3358,7 @@ chat item action Error Chyba - No comment provided by engineer. + conn error description Error aborting address change @@ -3083,6 +3383,10 @@ chat item action Chyba přidávání člena(ů) No comment provided by engineer. + + Error adding relay + alert title + Error adding server alert title @@ -3135,6 +3439,10 @@ chat item action Chyba při vytváření adresy No comment provided by engineer. + + Error creating channel + alert title + Error creating group Chyba při vytváření skupiny @@ -3261,10 +3569,6 @@ chat item action Error opening chat No comment provided by engineer. - - Error opening group - No comment provided by engineer. - Error receiving file Chyba při příjmu souboru @@ -3304,6 +3608,10 @@ chat item action Chyba při ukládání serverů ICE No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list alert title @@ -3364,6 +3672,10 @@ chat item action Chyba nastavování potvrzení o doručení! No comment provided by engineer. + + Error sharing channel + alert title + Error starting chat Chyba při spuštění chatu @@ -3438,7 +3750,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3655,7 +3968,8 @@ snd error text Fingerprint in server address does not match certificate. Otisk certifikátu v adrese serveru neodpovídá. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3695,9 +4009,14 @@ snd error text For all moderators No comment provided by engineer. + + For anyone to reach you + No comment provided by engineer. + For chat profile %@: - servers error + servers error +servers warning For console @@ -3816,10 +4135,18 @@ Error: %2$@ GIFy a nálepky No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. No comment provided by engineer. + + Get started + No comment provided by engineer. + Good afternoon! message preview @@ -3874,7 +4201,7 @@ Error: %2$@ Group link Odkaz na skupinu - No comment provided by engineer. + chat link info line Group links @@ -3945,7 +4272,7 @@ Error: %2$@ Hidden chat profiles - Skryté chat profily + Skryté profily chatu No comment provided by engineer. @@ -3982,6 +4309,10 @@ Error: %2$@ History is not sent to new members. No comment provided by engineer. + + History is not sent to new subscribers. + No comment provided by engineer. + How SimpleX works Jak SimpleX funguje @@ -4075,11 +4406,6 @@ Error: %2$@ Ihned No comment provided by engineer. - - Immune to spam - Odolná vůči spamu a zneužití - No comment provided by engineer. - Import Import @@ -4210,9 +4536,9 @@ More improvements are coming soon! Počáteční role No comment provided by engineer. - - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - Nainstalujte [SimpleX Chat pro terminál](https://github.com/simplex-chat/simplex-chat) + + Install SimpleX Chat for terminal + Nainstalujte SimpleX Chat pro terminál No comment provided by engineer. @@ -4263,7 +4589,7 @@ More improvements are coming soon! Invalid connection link Neplatný odkaz na spojení - No comment provided by engineer. + conn error description Invalid display name! @@ -4279,7 +4605,15 @@ More improvements are coming soon! Invalid name! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4314,6 +4648,10 @@ More improvements are coming soon! Pozvat členy No comment provided by engineer. + + Invite someone privately + No comment provided by engineer. + Invite to chat No comment provided by engineer. @@ -4388,6 +4726,10 @@ More improvements are coming soon! připojit se jako %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Připojit ke skupině @@ -4467,6 +4809,14 @@ This is your link for group %@! Opustit swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat No comment provided by engineer. @@ -4489,6 +4839,10 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let someone connect to you + No comment provided by engineer. + Let's talk in SimpleX Chat Promluvme si v SimpleX Chatu @@ -4508,6 +4862,10 @@ This is your link for group %@! Link mobile and desktop apps! 🔗 No comment provided by engineer. + + Link signature verified. + owner verification + Linked desktop options No comment provided by engineer. @@ -4675,6 +5033,10 @@ This is your link for group %@! Členové skupin mohou přidávat reakce na zprávy. No comment provided by engineer. + + Members can chat with admins. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin) @@ -4735,6 +5097,10 @@ This is your link for group %@! Návrh zprávy No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded item status text @@ -4817,6 +5183,14 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + E2EE info chat item + Messages in this chat will never be deleted. alert message @@ -4841,12 +5215,12 @@ This is your link for group %@! Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery. No comment provided by engineer. - - Migrate device + + Migrate No comment provided by engineer. - - Migrate from another device + + Migrate device No comment provided by engineer. @@ -4933,7 +5307,7 @@ This is your link for group %@! Multiple chat profiles - Více chatovacích profilů + Více profilů chatu No comment provided by engineer. @@ -4960,6 +5334,10 @@ This is your link for group %@! Síť a servery No comment provided by engineer. + + Network commitments + No comment provided by engineer. + Network connection No comment provided by engineer. @@ -4968,6 +5346,10 @@ This is your link for group %@! Network decentralization No comment provided by engineer. + + Network error + conn error description + Network issues - message expired after many attempts to send it. snd error text @@ -4980,6 +5362,11 @@ This is your link for group %@! Network operator No comment provided by engineer. + + Network routers cannot know +who talks to whom + No comment provided by engineer. + Network settings Nastavení sítě @@ -4994,6 +5381,10 @@ This is your link for group %@! New token status text + + New 1-time link + No comment provided by engineer. + New Passcode Nové heslo @@ -5015,6 +5406,10 @@ This is your link for group %@! New chat experience 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Žádost o nový kontakt @@ -5080,11 +5475,28 @@ This is your link for group %@! Ne No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + No comment provided by engineer. + + + No active relays + No comment provided by engineer. + No app password Žádné heslo aplikace Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats No comment provided by engineer. @@ -5211,11 +5623,24 @@ This is your link for group %@! No unread chats No comment provided by engineer. - - No user identifiers. - Bez uživatelských identifikátorů. + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. + Nikdo nesledoval vaše konverzace. Nikdo nevytvořil mapu, kde jste byli. Soukromí nikdy nebylo funkcí - byl to způsob života. No comment provided by engineer. + + Non-profit governance + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + Nejde o to mít lepší zámek na dveřích někoho jiného. Ani o to mít nájemce, který respektuje vaše soukromí, ale vede evidenci všech vašich návštěvníků. Nejste host. Jste doma. Ani král k vám nemůže vstoupit - jste suverén. + No comment provided by engineer. + + + Not all relays connected + alert title + Not compatible! No comment provided by engineer. @@ -5265,7 +5690,7 @@ This is your link for group %@! OK - No comment provided by engineer. + alert button Off @@ -5284,11 +5709,19 @@ new chat action Stará databáze No comment provided by engineer. + + On your phone, not on servers. + No comment provided by engineer. + One-time invitation link Jednorázový zvací odkaz No comment provided by engineer. + + One-time link + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5308,6 +5741,10 @@ Vyžaduje povolení sítě VPN. Onion hostitelé nebudou použiti. No comment provided by engineer. + + Only channel owners can change channel preferences. + No comment provided by engineer. + Only chat owners can change preferences. No comment provided by engineer. @@ -5405,7 +5842,8 @@ Vyžaduje povolení sítě VPN. Open Otevřít - alert action + alert action +alert button Open Settings @@ -5416,6 +5854,10 @@ Vyžaduje povolení sítě VPN. Open changes No comment provided by engineer. + + Open channel + new chat action + Open chat Otevřete chat @@ -5434,6 +5876,10 @@ Vyžaduje povolení sítě VPN. Open conditions No comment provided by engineer. + + Open external link? + alert title + Open full link alert action @@ -5450,6 +5896,10 @@ Vyžaduje povolení sítě VPN. Open migration to another device authentication reason + + Open new channel + new chat action + Open new chat new chat action @@ -5486,6 +5936,13 @@ Vyžaduje povolení sítě VPN. Operator server alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + No comment provided by engineer. + Or import archive file No comment provided by engineer. @@ -5502,6 +5959,10 @@ Vyžaduje povolení sítě VPN. Or securely share this file link No comment provided by engineer. + + Or show QR in person or via video call. + No comment provided by engineer. + Or show this code No comment provided by engineer. @@ -5510,6 +5971,10 @@ Vyžaduje povolení sítě VPN. Or to share privately No comment provided by engineer. + + Or use this QR - print or show online. + No comment provided by engineer. + Organize chats into lists No comment provided by engineer. @@ -5523,6 +5988,18 @@ Vyžaduje povolení sítě VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + + + Ownership: you can run your own relays. + No comment provided by engineer. + PING count Počet PING @@ -5576,6 +6053,10 @@ Vyžaduje povolení sítě VPN. Vložit obrázek No comment provided by engineer. + + Paste link / Scan + No comment provided by engineer. + Paste link to connect! No comment provided by engineer. @@ -5714,6 +6195,14 @@ Error: %@ Zachování posledního návrhu zprávy s přílohami. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Přednastavená adresa serveru @@ -5745,13 +6234,12 @@ Error: %@ Privacy policy and conditions of use. No comment provided by engineer. - - Privacy redefined - Nové vymezení soukromí + + Privacy: for owners and subscribers. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. + + Private and secure messaging. No comment provided by engineer. @@ -5787,6 +6275,10 @@ Error: %@ Private routing timeout alert title + + Proceed + alert action + Profile and server connections Profil a připojení k serveru @@ -5810,9 +6302,8 @@ Error: %@ Profile theme No comment provided by engineer. - - Profile update will be sent to your contacts. - Aktualizace profilu bude zaslána vašim kontaktům. + + Profile update will be sent to your SimpleX contacts. alert message @@ -5820,6 +6311,10 @@ Error: %@ Zákaz audio/video hovorů. No comment provided by engineer. + + Prohibit chats with admins. + No comment provided by engineer. + Prohibit irreversible message deletion. Zakázat nevratné mazání zpráv. @@ -5848,6 +6343,10 @@ Error: %@ Zakázat odesílání přímých zpráv členům. No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + No comment provided by engineer. + Prohibit sending disappearing messages. Zakázat posílání mizících zpráv. @@ -5879,7 +6378,7 @@ Enable in *Network & servers* settings. Protect your chat profiles with a password! - Chraňte své chat profily heslem! + Chraňte své profily chatu pomocí hesla! No comment provided by engineer. @@ -5908,6 +6407,10 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. + + Public channels - speak freely 🚀 + No comment provided by engineer. + Push notifications Nabízená oznámení @@ -5945,23 +6448,14 @@ Enable in *Network & servers* settings. Přečíst více No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + Více informací v průvodci uživatele. No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - Další informace naleznete v [Uživatelské příručce](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - No comment provided by engineer. - - - Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - Přečtěte si více v [Uživatelské příručce](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). - Přečtěte si více v našem [GitHub repozitáři](https://github.com/simplex-chat/simplex-chat#readme). + + Read more in our GitHub repository. + Přečtěte si více v našem GitHub repozitáři. No comment provided by engineer. @@ -6106,6 +6600,26 @@ swipe action Reject member? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + + + Relay results: + alert message + Relay server is only used if necessary. Another party can observe your IP address. Přenosový server se používá pouze v případě potřeby. Jiná strana může sledovat vaši IP adresu. @@ -6116,6 +6630,14 @@ swipe action Přenosový server chrání vaši IP adresu, ale může sledovat dobu trvání hovoru. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + + + Reliability: many relays per channel. + No comment provided by engineer. + Remove Odstranit @@ -6152,6 +6674,14 @@ swipe action Odstranit přístupovou frázi z klíčenek? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. No comment provided by engineer. @@ -6274,7 +6804,7 @@ swipe action Restart the app to create a new chat profile - Restartujte aplikaci pro vytvoření nového chat profilu + Restartujte aplikaci pro vytvoření nového profilu chatu No comment provided by engineer. @@ -6360,6 +6890,10 @@ swipe action SOCKS proxy No comment provided by engineer. + + Safe web links + No comment provided by engineer. + Safely receive files No comment provided by engineer. @@ -6383,6 +6917,10 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + alert button + Save admission settings? alert title @@ -6397,6 +6935,10 @@ chat item action Uložit a upozornit členy skupiny No comment provided by engineer. + + Save and notify subscribers + No comment provided by engineer. + Save and reconnect No comment provided by engineer. @@ -6406,6 +6948,14 @@ chat item action Uložit a aktualizovat profil skupiny No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Uložení profilu skupiny @@ -6567,6 +7117,10 @@ chat item action Bezpečnostní kód No comment provided by engineer. + + Security: owners hold channel keys. + No comment provided by engineer. + Select Vybrat @@ -6686,6 +7240,10 @@ chat item action Send request without message No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. Odeslat je z galerie nebo vlastní klávesnice. @@ -6695,6 +7253,10 @@ chat item action Send up to 100 last messages to new members. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + No comment provided by engineer. + Send your private feedback to groups. No comment provided by engineer. @@ -6709,9 +7271,13 @@ chat item action Odesílatel možná smazal požadavek připojení. No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. - Odesílání potvrzení o doručení bude povoleno pro všechny kontakty ve všech viditelných chat profilech. + Odesílání potvrzení o doručení bude povoleno pro všechny kontakty ve všech viditelných profilech chatu. No comment provided by engineer. @@ -6821,6 +7387,10 @@ chat item action Server protocol changed. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo. @@ -6938,6 +7508,14 @@ chat item action Settings were changed. alert message + + Setup notifications + No comment provided by engineer. + + + Setup routers + No comment provided by engineer. + Shape profile images No comment provided by engineer. @@ -6970,11 +7548,14 @@ chat item action Share address publicly No comment provided by engineer. - - Share address with contacts? - Sdílet adresu s kontakty? + + Share address with SimpleX contacts? alert title + + Share channel + No comment provided by engineer. + Share from other apps. No comment provided by engineer. @@ -6996,6 +7577,10 @@ chat item action Share profile No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. @@ -7004,9 +7589,12 @@ chat item action Share to SimpleX No comment provided by engineer. - - Share with contacts - Sdílet s kontakty + + Share via chat + No comment provided by engineer. + + + Share with SimpleX contacts No comment provided by engineer. @@ -7162,8 +7750,8 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7230,6 +7818,11 @@ report reason Square, circle, or anything in between. No comment provided by engineer. + + Star on GitHub + Hvězda na GitHubu + No comment provided by engineer. + Start chat Začít chat @@ -7322,6 +7915,63 @@ report reason Subscribed No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber reports + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers can add message reactions. + No comment provided by engineer. + + + Subscribers can chat with admins. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + No comment provided by engineer. + + + Subscribers can send direct messages. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + No comment provided by engineer. + + + Subscribers can send files and media. + No comment provided by engineer. + + + Subscribers can send voice messages. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors No comment provided by engineer. @@ -7394,6 +8044,10 @@ report reason Vyfotit No comment provided by engineer. + + Talk to someone + No comment provided by engineer. + Tap Connect to chat No comment provided by engineer. @@ -7406,8 +8060,8 @@ report reason Tap Connect to use bot No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. + + Tap Join channel No comment provided by engineer. @@ -7438,6 +8092,10 @@ report reason Klepnutím se připojíte inkognito No comment provided by engineer. + + Tap to open + No comment provided by engineer. + Tap to paste link No comment provided by engineer. @@ -7453,12 +8111,17 @@ report reason Test failed at step %@. Test selhal v kroku %@. - server test failure + relay test failure +server test failure Test notifications No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Testovací server @@ -7509,6 +8172,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován The app protects your privacy by using different operators in each conversation. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -7522,6 +8189,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. No comment provided by engineer. @@ -7546,9 +8217,9 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Šifrování funguje a nové povolení šifrování není vyžadováno. To může vyvolat chybu v připojení! No comment provided by engineer. - - The future of messaging - Nová generace soukromých zpráv + + The first network where you own +your contacts and groups. No comment provided by engineer. @@ -7583,6 +8254,11 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Stará databáze nebyla během přenášení odstraněna, lze ji smazat. No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + Nejstarší lidská svoboda - mluvit s druhým člověkem, aniž by byl sledován - postavena na infrastruktuře, která ji nemůže zradit. + No comment provided by engineer. + The same conditions will apply to operator **%@**. No comment provided by engineer. @@ -7603,7 +8279,7 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován The servers for new connections of your current chat profile **%@**. - Servery pro nová připojení vašeho aktuálního chat profilu **%@**. + Servery pro nová připojení vašeho aktuálního profilu chatu **%@**. No comment provided by engineer. @@ -7622,6 +8298,16 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Themes No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + Pak jsme se přesunuli na internet a každá platforma chtěla o vás něco vědět - vaše jméno, vaše číslo, vaše přátele. Smířili jsme se s tím, že cenou za komunikaci s ostatními je dát někomu vědět, s kým mluvíme. Každá generace, lidská i technická, to tak měla - telefon, e-mail, komunikátory, sociální sítě. Zdálo se, že je to jediný možný způsob. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + Existuje i jiný způsob. Síť bez telefonních čísel. Bez uživatelských jmen. Bez účtů. Bez jakékoli uživatelské identity. Síť, která spojuje lidi a přenáší šifrované zprávy, aniž by bylo známo, kdo je připojen. + No comment provided by engineer. + These conditions will also apply for: **%@**. No comment provided by engineer. @@ -7681,6 +8367,14 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Tato skupina již neexistuje. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -7695,7 +8389,7 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován This setting applies to messages in your current chat profile **%@**. - Toto nastavení platí pro zprávy ve vašem aktuálním chat profilu **%@**. + Toto nastavení platí pro zprávy ve vašem aktuálním profilu chatu **%@**. No comment provided by engineer. @@ -7724,6 +8418,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován To hide unwanted messages. No comment provided by engineer. + + To make SimpleX Network last. + No comment provided by engineer. + To make a new connection Vytvoření nového připojení @@ -7773,7 +8471,7 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. - Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce **Chat profily**. + Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce **Profily chatu**. No comment provided by engineer. @@ -7802,10 +8500,6 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Chcete-li ověřit koncové šifrování u svého kontaktu, porovnejte (nebo naskenujte) kód na svých zařízeních. No comment provided by engineer. - - Toggle chat list: - No comment provided by engineer. - Toggle incognito when connecting. Změnit inkognito režim při připojení. @@ -7819,6 +8513,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Toolbar opacity No comment provided by engineer. + + Top bar + No comment provided by engineer. + Total No comment provided by engineer. @@ -7875,6 +8573,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Unblock member? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -7896,7 +8598,7 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Unhide chat profile - Odkrýt chat profil + Odkrýt profil chatu No comment provided by engineer. @@ -7970,12 +8672,16 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Unsupported connection link - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + No comment provided by engineer. + Update Aktualizovat @@ -8084,11 +8790,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use TCP port 443 for preset servers only. No comment provided by engineer. - - Use chat - Použijte chat - No comment provided by engineer. - Use current profile Použít aktuální profil @@ -8102,6 +8803,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use for messages No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Použít pro nová připojení @@ -8137,6 +8842,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use private routing with unknown servers. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Použít server @@ -8154,6 +8863,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use the app with one hand. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + No comment provided by engineer. + Use web port No comment provided by engineer. @@ -8171,6 +8884,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Používat servery SimpleX Chat. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop No comment provided by engineer. @@ -8280,6 +8997,18 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Hlasová zpráva… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + + + Waiting for channel owner to add relays. + No comment provided by engineer. + Waiting for desktop... No comment provided by engineer. @@ -8316,6 +9045,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Upozornění: můžete ztratit nějaká data! No comment provided by engineer. + + We made connecting simpler for new users. + No comment provided by engineer. + WebRTC ICE servers WebRTC servery ICE @@ -8362,6 +9095,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Pokud s někým sdílíte inkognito profil, bude tento profil použit pro skupiny, do kterých vás pozve. No comment provided by engineer. + + Why SimpleX is built. + No comment provided by engineer. + WiFi No comment provided by engineer. @@ -8546,9 +9283,13 @@ Repeat join request? Náhled oznámení na zamykací obrazovce můžete změnit v nastavení. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + 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. - Můžete sdílet odkaz nebo QR kód - ke skupině se bude moci připojit kdokoli. O členy skupiny nepřijdete, pokud ji později odstraníte. + Můžete sdílet odkaz nebo QR kód - ke skupině se bude moci připojit kdokoli. O členy skupiny nepřijdete, pokud odkaz později smažete. No comment provided by engineer. @@ -8588,16 +9329,21 @@ Repeat join request? Nemůžete posílat zprávy! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Nemohli jste být ověřeni; Zkuste to prosím znovu. No comment provided by engineer. - - You decide who can connect. - Lidé se s vámi mohou spojit pouze prostřednictvím odkazu, který sdílíte. - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -8659,6 +9405,11 @@ Repeat connection request? You should receive notifications. token info + + You were born without an account + Narodili jste se bez účtu. + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. No comment provided by engineer. @@ -8692,6 +9443,10 @@ Repeat connection request? Stále budete přijímat volání a upozornění od umlčených profilů pokud budou aktivní. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. No comment provided by engineer. @@ -8735,6 +9490,10 @@ Repeat connection request? Vaše hovory No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Vaše chatovací databáze @@ -8751,7 +9510,7 @@ Repeat connection request? Your chat profiles - Vaše chat profily + Vaše profily chatu No comment provided by engineer. @@ -8781,6 +9540,11 @@ Repeat connection request? Vaše kontakty zůstanou připojeny. No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + Vaše konverzace patří vám, jako tomu bylo vždy před internetem. Síť není místo, které navštěvujete. Je to místo, které vytváříte a vlastníte. A nikdo vám ho nemůže vzít, ať už je soukromé, nebo veřejné. + No comment provided by engineer. + Your credentials may be sent unencrypted. No comment provided by engineer. @@ -8799,6 +9563,10 @@ Repeat connection request? Your group No comment provided by engineer. + + Your network + No comment provided by engineer. + Your preferences Vaše preference @@ -8813,6 +9581,11 @@ Repeat connection request? Your profile No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Váš profil **%@** bude sdílen. @@ -8832,11 +9605,23 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. alert message + + Your public address + No comment provided by engineer. + Your random profile Váš náhodný profil No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Adresa vašeho serveru @@ -8851,21 +9636,11 @@ Repeat connection request? Vaše nastavení No comment provided by engineer. - - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - [Přispějte](https://github.com/simplex-chat/simplex-chat#contribute) - No comment provided by engineer. - [Send us email](mailto:chat@simplex.chat) [Pošlete nám e-mail](mailto:chat@simplex.chat) No comment provided by engineer. - - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - [Hvězda na GitHubu](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - \_italic_ \_kurzíva_ @@ -8881,6 +9656,10 @@ Repeat connection request? výše, pak vyberte: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ rcv group event chat item @@ -8898,6 +9677,10 @@ Repeat connection request? accepted you rcv group event chat item + + active + No comment provided by engineer. + admin správce @@ -8998,6 +9781,10 @@ marked deleted chat item preview text volání… call status + + can't broadcast + No comment provided by engineer. + can't send messages No comment provided by engineer. @@ -9032,6 +9819,14 @@ marked deleted chat item preview text změna adresy… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored barevné @@ -9172,6 +9967,10 @@ pref value smazáno deleted chat item + + deleted channel + rcv group event chat item + deleted contact rcv direct event chat item @@ -9280,6 +10079,10 @@ pref value chyba No comment provided by engineer. + + error: %@ + receive error chat item + expired No comment provided by engineer. @@ -9403,6 +10206,10 @@ pref value opustil rcv group event chat item + + link + No comment provided by engineer. + marked deleted označeno jako smazáno @@ -9469,6 +10276,10 @@ pref value nikdy delete after time + + new + No comment provided by engineer. + new message nová zpráva @@ -9582,6 +10393,10 @@ time to disappear odmítnutý hovor call status + + relay + member role + removed odstraněno @@ -9592,6 +10407,14 @@ time to disappear odstraněno %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + + + removed by operator + No comment provided by engineer. + removed contact address profile update event chat item @@ -9723,6 +10546,10 @@ last received msg: %2$@ unprotected No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile aktualizoval profil skupiny @@ -9741,6 +10568,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link prostřednictvím odkazu na kontaktní adresu @@ -9812,6 +10643,10 @@ last received msg: %2$@ jste pozorovatel No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ snd group event chat item @@ -9870,6 +10705,10 @@ last received msg: %2$@ \~stávka~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + owner verification + 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 7ab0535158..872fafddd7 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -185,6 +185,24 @@ %d Monate time interval + + %d relays failed + %d Relais fehlgeschlagen + channel relay bar +channel subscriber relay bar + + + %d relays not active + %d Relais nicht aktiv + channel relay bar +channel subscriber relay bar + + + %d relays removed + %d Relais entfernt + channel relay bar +channel subscriber relay bar + %d sec %d s @@ -200,11 +218,63 @@ %d übersprungene Nachricht(en) integrity error chat item + + %d subscriber + %d Abonnent + channel subscriber count + + + %d subscribers + %d Abonnenten + channel subscriber count + %d weeks %d Wochen time interval + + %1$d/%2$d relays active + %1$d/%2$d Relais aktiv + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + %1$d/%2$d Relais aktiv, %3$d Fehler + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + %1$d/%2$d Relais aktiv, %3$d fehlgeschlagen + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + %1$d/%2$d Relais aktiv, %3$d entfernt + channel relay bar + + + %1$d/%2$d relays connected + %1$d/%2$d Relais verbunden + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + %1$d/%2$d Relais verbunden, %3$d Fehler + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + %1$d/%2$d Relais verbunden, %3$d fehlgeschlagen + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + %1$d/%2$d Relais verbunden, %3$d entfernt + channel subscriber relay bar + %lld %lld @@ -215,6 +285,11 @@ %lld %@ No comment provided by engineer. + + %lld channel events + %lld Kanalereignisse + No comment provided by engineer. + %lld contact(s) selected %lld Kontakt(e) ausgewählt @@ -315,11 +390,21 @@ %u übersprungene Nachrichten. No comment provided by engineer. + + (from owner) + (vom Eigentümer) + chat link info line + (new) (Neu) No comment provided by engineer. + + (signed) + (signiert) + chat link info line + (this device v%@) (Dieses Gerät hat v%@) @@ -365,6 +450,11 @@ **Link scannen / einfügen**: Um eine Verbindung über den Link herzustellen, den Sie erhalten haben. No comment provided by engineer. + + **Test relay** to retrieve its name. + **Relais testen** um seinen Namen abzurufen. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Warnung**: Sofortige Push-Benachrichtigungen erfordern die Eingabe eines Passworts, welches in Ihrem Schlüsselbund gespeichert ist. @@ -408,6 +498,15 @@ - und mehr! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + - Opt‑in zum Senden von Linkvorschauen. +- Hyperlink‑Phishing verhindern. +- Link‑Tracking entfernen. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -506,6 +605,11 @@ time interval Ein paar weitere Dinge No comment provided by engineer. + + A link for one person to connect + Verbindungs-Link für eine Person + No comment provided by engineer. + A new contact Ein neuer Kontakt @@ -632,9 +736,9 @@ swipe action Aktive Verbindungen 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. - Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet. + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. + Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre SimpleX-Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre SimpleX-Kontakte gesendet. No comment provided by engineer. @@ -702,6 +806,11 @@ swipe action Nachrichtenserver hinzugefügt No comment provided by engineer. + + Adding relays will be supported later. + Das Hinzufügen von Relais wird zu einem späteren Zeitpunkt unterstützt. + No comment provided by engineer. + Additional accent Erste Akzentfarbe @@ -744,7 +853,7 @@ swipe action Admins can create the links to join groups. - Administratoren können Links für den Beitritt zu Gruppen erzeugen. + Administratoren können Links für den Beitritt zu Gruppen erstellen. No comment provided by engineer. @@ -822,6 +931,16 @@ swipe action Alle Profile profile dropdown + + All relays failed + Alle Relais fehlgeschlagen + No comment provided by engineer. + + + All relays removed + Alle Relais entfernt + No comment provided by engineer. + All reports will be archived for you. Alle Meldungen werden für Sie archiviert. @@ -882,6 +1001,11 @@ swipe action Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt. (24 Stunden) No comment provided by engineer. + + Allow members to chat with admins. + Mitgliedern den Chat mit Administratoren erlauben. + No comment provided by engineer. + Allow message reactions only if your contact allows them. Erlauben Sie Reaktionen auf Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt. @@ -897,6 +1021,11 @@ swipe action Das Senden von Direktnachrichten an Gruppenmitglieder erlauben. No comment provided by engineer. + + Allow sending direct messages to subscribers. + Das Senden von Direktnachrichten an Abonnenten erlauben. + No comment provided by engineer. + Allow sending disappearing messages. Das Senden von verschwindenden Nachrichten erlauben. @@ -907,6 +1036,11 @@ swipe action Teilen erlauben No comment provided by engineer. + + Allow subscribers to chat with admins. + Abonnenten den Chat mit Administratoren erlauben. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Unwiederbringliches löschen von gesendeten Nachrichten erlauben. (24 Stunden) @@ -1012,11 +1146,6 @@ swipe action Anruf annehmen No comment provided by engineer. - - Anybody can host servers. - Jeder kann seine eigenen Server aufsetzen. - No comment provided by engineer. - App build: %@ App Build: %@ @@ -1222,6 +1351,23 @@ swipe action Ungültiger Nachrichten-Hash No comment provided by engineer. + + Be free +in your network + Seien Sie frei +in Ihrem Netzwerk + No comment provided by engineer. + + + Be free in your network. + Genießen Sie die Freiheit in Ihrem Netzwerk. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + Weil wir die Macht zerstört haben, zu wissen, wer Sie sind. Damit Ihnen Ihre Macht niemals genommen werden kann. + No comment provided by engineer. + Better calls Verbesserte Anrufe @@ -1317,6 +1463,11 @@ swipe action Mitglied blockieren? No comment provided by engineer. + + Block subscriber for all? + Abonnent für alle blockieren? + No comment provided by engineer. + Blocked by admin wurde vom Administrator blockiert @@ -1367,6 +1518,16 @@ swipe action Sowohl Ihr Kontakt, als auch Sie können Sprachnachrichten senden. No comment provided by engineer. + + Bottom bar + Untere Leiste + No comment provided by engineer. + + + Broadcast + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Bulgarisch, Finnisch, Thailändisch und Ukrainisch - Dank der Nutzer und [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1375,7 +1536,7 @@ swipe action Business address Geschäftliche Adresse - No comment provided by engineer. + chat link info line Business chats @@ -1397,15 +1558,6 @@ swipe action Per Chat-Profil (Voreinstellung) oder [per Verbindung](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - Durch die Nutzung von SimpleX Chat erklären Sie sich damit einverstanden: -- nur legale Inhalte in öffentlichen Gruppen zu versenden. -- andere Nutzer zu respektieren - kein Spam. - No comment provided by engineer. - Call already ended! Anruf ist bereits beendet! @@ -1554,6 +1706,82 @@ new chat action authentication reason set passcode view + + Channel + Kanal + No comment provided by engineer. + + + Channel display name + Anzeigename des Kanals + No comment provided by engineer. + + + Channel full name (optional) + Vollständiger Kanalname (optional) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + Der Kanal hat keine aktiven Relais. Bitte später erneut versuchen. + alert message +alert subtitle + + + Channel image + Kanalbild + No comment provided by engineer. + + + Channel link + Kanallink + chat link info line + + + Channel preferences + Kanal-Präferenzen + No comment provided by engineer. + + + Channel profile + Kanalprofil + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + Das Kanalprofil wird auf den Geräten der Abonnenten und auf den Chat‑Relais gespeichert. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + Das Kanalprofil wurde geändert. Beim Speichern wird das aktualisierte Profil an die Abonnenten des Kanals gesendet. + alert message + + + Channel temporarily unavailable + Der Kanal ist vorübergehend nicht erreichbar + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + Der Kanal wird für alle Abonnenten gelöscht. Dies kann nicht rückgängig gemacht werden! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + Der Kanal wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + Der Kanal wird mit %1$d von %2$d Relais gestartet. Fortfahren? + alert message + + + Channels + Kanäle + No comment provided by engineer. + Chat Chat @@ -1639,6 +1867,26 @@ set passcode view Benutzerprofil No comment provided by engineer. + + Chat relay + Chat-Relais + No comment provided by engineer. + + + Chat relays + Chat-Relais + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + Chat‑Relais leiten Nachrichten in den von Ihnen erstellten Kanälen weiter. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + Chat‑Relais leiten Nachrichten an Kanal-Abonnenten weiter. + No comment provided by engineer. + Chat theme Chat-Design @@ -1657,7 +1905,8 @@ set passcode view Chat with admins Chat mit Administratoren - chat toolbar + chat feature +chat toolbar Chat with member @@ -1674,11 +1923,26 @@ set passcode view Chats No comment provided by engineer. + + Chats with admins are prohibited. + Chats mit Administratoren sind nicht erlaubt. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + Chats mit Administratoren in öffentlichen Kanälen sind nicht Ende‑zu‑Ende‑verschlüsselt – bitte nur über vertrauenswürdige Chat‑Relais nutzen. + alert message + Chats with members Chats mit Mitgliedern No comment provided by engineer. + + Chats with members are disabled + Chats mit Mitgliedern sind deaktiviert + No comment provided by engineer. + Check messages every 20 min. Alle 20min Nachrichten überprüfen. @@ -1689,6 +1953,16 @@ set passcode view Wenn es erlaubt ist, Nachrichten überprüfen. No comment provided by engineer. + + Check relay address and try again. + Relais-Adresse überprüfen und erneut versuchen. + alert message + + + Check relay name and try again. + Relais-Name überprüfen und erneut versuchen. + alert message + Check server address and try again. Überprüfen Sie die Serveradresse und versuchen Sie es nochmal. @@ -1834,9 +2108,9 @@ set passcode view ICE-Server konfigurieren No comment provided by engineer. - - Configure server operators - Server-Betreiber konfigurieren + + Configure relays + Relais konfigurieren No comment provided by engineer. @@ -1897,7 +2171,8 @@ set passcode view Connect Verbinden - server test step + relay test step +server test step Connect automatically @@ -1943,6 +2218,11 @@ Das ist Ihr eigener Einmal-Link! Über einen Link verbinden new chat sheet title + + Connect via link or QR code + Über einen Link oder QR-Code verbinden + No comment provided by engineer. + Connect via one-time link Über einen Einmal-Link verbinden @@ -2021,10 +2301,11 @@ Das ist Ihr eigener Einmal-Link! Connection error (AUTH) Verbindungsfehler (AUTH) - No comment provided by engineer. + conn error description Connection failed + Verbindung fehlgeschlagen No comment provided by engineer. @@ -2079,6 +2360,11 @@ Das ist Ihr eigener Einmal-Link! Verbindungen No comment provided by engineer. + + Contact address + Kontaktadresse + chat link info line + Contact allows Der Kontakt erlaubt @@ -2149,6 +2435,11 @@ Das ist Ihr eigener Einmal-Link! Weiter No comment provided by engineer. + + Contribute + Unterstützen Sie uns + No comment provided by engineer. + Conversation deleted! Chat-Inhalte entfernt! @@ -2177,12 +2468,7 @@ Das ist Ihr eigener Einmal-Link! Correct name to %@? Richtiger Name für %@? - No comment provided by engineer. - - - Create - Erstellen - No comment provided by engineer. + alert message Create 1-time link @@ -2196,7 +2482,7 @@ Das ist Ihr eigener Einmal-Link! Create a group using a random profile. - Erstellen Sie eine Gruppe mit einem zufälligen Profil. + Gruppe mit einem zufälligen Profil erstellen. No comment provided by engineer. @@ -2216,7 +2502,7 @@ Das ist Ihr eigener Einmal-Link! Create link - Link erzeugen + Link erstellen No comment provided by engineer. @@ -2234,9 +2520,19 @@ Das ist Ihr eigener Einmal-Link! Profil erstellen No comment provided by engineer. + + Create public channel + Öffentlichen Kanal erstellen + No comment provided by engineer. + + + Create public channel (BETA) + Öffentlichen Kanal erstellen (BETA) + No comment provided by engineer. + Create queue - Erzeuge Warteschlange + Warteschlange erstellen server test step @@ -2244,9 +2540,19 @@ Das ist Ihr eigener Einmal-Link! Ihre Adresse erstellen No comment provided by engineer. + + Create your link + Ihren Link erstellen + No comment provided by engineer. + Create your profile - Erstellen Sie Ihr Profil + Ihr Profil erstellen + No comment provided by engineer. + + + Create your public address + Ihre öffentliche Adresse erstellen No comment provided by engineer. @@ -2269,6 +2575,11 @@ Das ist Ihr eigener Einmal-Link! Archiv-Link erzeugen No comment provided by engineer. + + Creating channel + Kanal wird erstellt + No comment provided by engineer. + Creating link… Link wird erstellt… @@ -2427,10 +2738,10 @@ Das ist Ihr eigener Einmal-Link! Debugging-Zustellung No comment provided by engineer. - - Decentralized - Dezentral - No comment provided by engineer. + + Decode link + Link dekodieren + relay test step Decryption error @@ -2478,6 +2789,16 @@ swipe action Kontakt löschen und benachrichtigen No comment provided by engineer. + + Delete channel + Kanal löschen + No comment provided by engineer. + + + Delete channel? + Kanal löschen? + No comment provided by engineer. + Delete chat Chat löschen @@ -2649,6 +2970,11 @@ alert button Lösche Warteschlange server test step + + Delete relay + Relais löschen + No comment provided by engineer. + Delete report Meldung löschen @@ -2814,6 +3140,16 @@ alert button In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt. No comment provided by engineer. + + Direct messages between subscribers are prohibited. + Direktnachrichten zwischen Abonnenten sind nicht erlaubt. + No comment provided by engineer. + + + Disable + Deaktivieren + alert button + Disable (keep overrides) Deaktivieren (vorgenommene Einstellungen bleiben erhalten) @@ -2919,6 +3255,11 @@ alert button Den Nachrichtenverlauf nicht an neue Mitglieder senden. No comment provided by engineer. + + Do not send history to new subscribers. + Den Nachrichtenverlauf nicht an neue Abonnenten senden. + No comment provided by engineer. + Do not use credentials with proxy. Verwenden Sie keine Anmeldeinformationen mit einem Proxy. @@ -3020,11 +3361,21 @@ chat item action E2E-verschlüsselte Benachrichtigungen. No comment provided by engineer. + + Easier to invite your friends 👋 + Freunde einladen – jetzt noch einfacher 👋 + No comment provided by engineer. + Edit Bearbeiten chat item action + + Edit channel profile + Kanalprofil bearbeiten + No comment provided by engineer. + Edit group profile Gruppenprofil bearbeiten @@ -3038,7 +3389,7 @@ chat item action Enable Aktivieren - No comment provided by engineer. + alert button Enable (keep overrides) @@ -3060,6 +3411,11 @@ chat item action TCP-Keep-alive aktivieren No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + Aktivieren Sie mindestens ein Chat‑Relais unter 'Netzwerk & Server'. + channel creation warning + Enable automatic message deletion? Automatisches Löschen von Nachrichten aktivieren? @@ -3070,6 +3426,11 @@ chat item action Kamera-Zugriff aktivieren No comment provided by engineer. + + Enable chats with admins? + Chats mit Administratoren aktivieren? + alert title + Enable disappearing messages by default. Verschwindende Nachrichten sind per Voreinstellung aktiviert. @@ -3090,16 +3451,16 @@ chat item action Sofortige Benachrichtigungen aktivieren? No comment provided by engineer. + + Enable link previews? + Linkvorschau aktivieren? + alert title + Enable lock Sperre aktivieren No comment provided by engineer. - - Enable notifications - Benachrichtigungen aktivieren - No comment provided by engineer. - Enable periodic notifications? Periodische Benachrichtigungen aktivieren? @@ -3205,6 +3566,11 @@ chat item action Zugangscode eingeben No comment provided by engineer. + + Enter channel name… + Kanalname eingeben… + No comment provided by engineer. + Enter correct passphrase. Geben Sie das korrekte Passwort ein. @@ -3230,6 +3596,16 @@ chat item action Für die Anzeige das Passwort im Suchfeld eingeben! No comment provided by engineer. + + Enter profile name... + Profilname eingeben... + No comment provided by engineer. + + + Enter relay name… + Relais-Name eingeben… + No comment provided by engineer. + Enter server manually Geben Sie den Server manuell ein @@ -3258,7 +3634,7 @@ chat item action Error Fehler - No comment provided by engineer. + conn error description Error aborting address change @@ -3285,6 +3661,11 @@ chat item action Fehler beim Hinzufügen von Mitgliedern No comment provided by engineer. + + Error adding relay + Fehler beim Hinzufügen des Relais + alert title + Error adding server Fehler beim Hinzufügen des Servers @@ -3345,6 +3726,11 @@ chat item action Fehler beim Erstellen der Adresse No comment provided by engineer. + + Error creating channel + Fehler beim Erstellen des Kanals + alert title + Error creating group Fehler beim Erzeugen der Gruppe @@ -3480,11 +3866,6 @@ chat item action Fehler beim Öffnen des Chat No comment provided by engineer. - - Error opening group - Fehler beim Vorbereiten der Gruppe - No comment provided by engineer. - Error receiving file Fehler beim Herunterladen der Datei @@ -3530,6 +3911,11 @@ chat item action Fehler beim Speichern der ICE-Server No comment provided by engineer. + + Error saving channel profile + Fehler beim Speichern des Kanalprofils + No comment provided by engineer. + Error saving chat list Fehler beim Speichern der Chat-Liste @@ -3595,6 +3981,11 @@ chat item action Fehler beim Setzen von Empfangsbestätigungen! No comment provided by engineer. + + Error sharing channel + Fehler beim Teilen des Kanals + alert title + Error starting chat Fehler beim Starten des Chats @@ -3675,7 +4066,8 @@ snd error text Error: %@. Fehler: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3919,7 +4311,8 @@ snd error text Fingerprint in server address does not match certificate. Fingerabdruck in der Serveradresse stimmt nicht mit dem Zertifikat überein. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3961,10 +4354,16 @@ snd error text Für alle Moderatoren No comment provided by engineer. + + For anyone to reach you + Damit Sie jeder erreichen kann + No comment provided by engineer. + For chat profile %@: Für das Chat-Profil %@: - servers error + servers error +servers warning For console @@ -4105,11 +4504,21 @@ Fehler: %2$@ GIFs und Sticker No comment provided by engineer. + + Get link + Link erhalten + relay test step + Get notified when mentioned. Bei Erwähnung benachrichtigt werden. No comment provided by engineer. + + Get started + Jetzt starten + No comment provided by engineer. + Good afternoon! Guten Nachmittag! @@ -4168,7 +4577,7 @@ Fehler: %2$@ Group link Gruppen-Link - No comment provided by engineer. + chat link info line Group links @@ -4280,6 +4689,11 @@ Fehler: %2$@ Der Nachrichtenverlauf wird nicht an neue Gruppenmitglieder gesendet. No comment provided by engineer. + + History is not sent to new subscribers. + Der Nachrichtenverlauf wird nicht an neue Abonnenten gesendet. + No comment provided by engineer. + How SimpleX works Wie SimpleX funktioniert @@ -4347,6 +4761,7 @@ Fehler: %2$@ If you joined or created channels, they will stop working permanently. + Kanäle, welche Sie erstellt haben oder denen Sie beigetreten sind, werden dauerhaft deaktiviert. down migration warning @@ -4379,11 +4794,6 @@ Fehler: %2$@ Sofort No comment provided by engineer. - - Immune to spam - Immun gegen Spam und Missbrauch - No comment provided by engineer. - Import Importieren @@ -4526,9 +4936,9 @@ Weitere Verbesserungen sind bald verfügbar! Anfängliche Rolle No comment provided by engineer. - - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - Installieren Sie [SimpleX Chat als Terminalanwendung](https://github.com/simplex-chat/simplex-chat) + + Install SimpleX Chat for terminal + Installieren Sie SimpleX Chat als Terminalanwendung No comment provided by engineer. @@ -4586,7 +4996,7 @@ Weitere Verbesserungen sind bald verfügbar! Invalid connection link Ungültiger Verbindungslink - No comment provided by engineer. + conn error description Invalid display name! @@ -4606,7 +5016,17 @@ Weitere Verbesserungen sind bald verfügbar! Invalid name! Ungültiger Name! - No comment provided by engineer. + alert title + + + Invalid relay address! + Ungültige Relais-Adresse! + alert title + + + Invalid relay name! + Ungültiger Relais-Name! + alert title Invalid response @@ -4643,6 +5063,11 @@ Weitere Verbesserungen sind bald verfügbar! Mitglieder einladen No comment provided by engineer. + + Invite someone privately + Für privaten Chat einladen + No comment provided by engineer. + Invite to chat Zum Chat einladen @@ -4719,6 +5144,11 @@ Weitere Verbesserungen sind bald verfügbar! Als %@ beitreten No comment provided by engineer. + + Join channel + Kanal beitreten + No comment provided by engineer. + Join group Treten Sie der Gruppe bei @@ -4806,6 +5236,16 @@ Das ist Ihr Link für die Gruppe %@! Verlassen swipe action + + Leave channel + Kanal verlassen + No comment provided by engineer. + + + Leave channel? + Kanal verlassen? + No comment provided by engineer. + Leave chat Chat verlassen @@ -4831,6 +5271,11 @@ Das ist Ihr Link für die Gruppe %@! Weniger Datenverkehr in mobilen Netzen. No comment provided by engineer. + + Let someone connect to you + Jemand mit Ihnen verbinden lassen + No comment provided by engineer. + Let's talk in SimpleX Chat Lassen Sie uns in SimpleX Chat kommunizieren @@ -4851,6 +5296,11 @@ Das ist Ihr Link für die Gruppe %@! Verknüpfe Mobiltelefon- und Desktop-Apps! 🔗 No comment provided by engineer. + + Link signature verified. + Linksignatur erfolgreich überprüft. + owner verification + Linked desktop options Verknüpfte Desktop-Optionen @@ -5036,6 +5486,11 @@ Das ist Ihr Link für die Gruppe %@! Gruppenmitglieder können eine Reaktion auf Nachrichten geben. No comment provided by engineer. + + Members can chat with admins. + Mitglieder können mit Administratoren chatten. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden) @@ -5101,6 +5556,11 @@ Das ist Ihr Link für die Gruppe %@! Nachrichtenentwurf No comment provided by engineer. + + Message error + Übertragungsfehler + No comment provided by engineer. + Message forwarded Nachricht weitergeleitet @@ -5196,6 +5656,16 @@ Das ist Ihr Link für die Gruppe %@! Die Nachrichten von %@ werden angezeigt! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + Nachrichten in diesem Kanal sind **nicht Ende‑zu‑Ende‑verschlüsselt**. Chat‑Relais können diese Nachrichten sehen. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + Nachrichten in diesem Kanal sind nicht Ende‑zu‑Ende‑verschlüsselt. Chat‑Relais können diese Nachrichten sehen. + E2EE info chat item + Messages in this chat will never be deleted. Nachrichten in diesem Chat werden nie gelöscht. @@ -5226,16 +5696,16 @@ Das ist Ihr Link für die Gruppe %@! Nachrichten, Dateien und Anrufe sind durch **Quantum-resistente E2E-Verschlüsselung** mit Perfect Forward Secrecy, Abstreitbarkeit und Wiederherstellung nach einer Kompromittierung geschützt. No comment provided by engineer. + + Migrate + Migrieren + No comment provided by engineer. + Migrate device Gerät migrieren No comment provided by engineer. - - Migrate from another device - Von einem anderen Gerät migrieren - No comment provided by engineer. - Migrate here Hierher migrieren @@ -5356,6 +5826,11 @@ Das ist Ihr Link für die Gruppe %@! Netzwerk & Server No comment provided by engineer. + + Network commitments + Netzwerk Verpflichtungen + No comment provided by engineer. + Network connection Netzwerkverbindung @@ -5366,6 +5841,11 @@ Das ist Ihr Link für die Gruppe %@! Dezentralisiertes Netzwerk No comment provided by engineer. + + Network error + Netzwerk-Fehler + conn error description + Network issues - message expired after many attempts to send it. Netzwerk-Fehler - die Nachricht ist nach vielen Sende-Versuchen abgelaufen. @@ -5381,6 +5861,13 @@ Das ist Ihr Link für die Gruppe %@! Netzwerk-Betreiber No comment provided by engineer. + + Network routers cannot know +who talks to whom + Netzwerk‑Router können nicht erkennen, +wer mit wem kommuniziert + No comment provided by engineer. + Network settings Netzwerkeinstellungen @@ -5396,6 +5883,11 @@ Das ist Ihr Link für die Gruppe %@! Neu token status text + + New 1-time link + Neuer Einmal-Link + No comment provided by engineer. + New Passcode Neuer Zugangscode @@ -5421,6 +5913,11 @@ Das ist Ihr Link für die Gruppe %@! Neue Chat-Erfahrung 🎉 No comment provided by engineer. + + New chat relay + Neues Chat-Relais + No comment provided by engineer. + New contact request Neue Kontaktanfrage @@ -5491,11 +5988,33 @@ Das ist Ihr Link für die Gruppe %@! Nein No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + Kein Account. Keine Telefonnummer. Keine E‑Mail. Keine ID. +Die sicherste Verschlüsselung. + No comment provided by engineer. + + + No active relays + Keine aktiven Relais + No comment provided by engineer. + No app password Kein App-Passwort Authentication unavailable + + No chat relays + Keine Chat-Relais + No comment provided by engineer. + + + No chat relays enabled. + Es sind keine Chat-Relais aktiviert. + servers warning + No chats Keine Chats @@ -5641,11 +6160,26 @@ Das ist Ihr Link für die Gruppe %@! Keine ungelesenen Chats No comment provided by engineer. - - No user identifiers. - Keine Benutzerkennungen. + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. + Niemand verfolgte Ihre Gespräche. Niemand erstellte eine Karte, wo Sie sich aufgehalten haben. Privatsphäre war nie ein Feature - sie war selbstverständlich. No comment provided by engineer. + + Non-profit governance + Non‑Profit‑Governance + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + Nicht ein besseres Schloss an der Tür eines Anderen. Kein freundlicher Vermieter, der Ihre Privatsphäre respektiert, aber dennoch jeden Besucher registriert. Sie sind kein Gast. Sie sind zu Hause. Kein Vermieter, kein Fremder kann es betreten - Sie sind souverän. + No comment provided by engineer. + + + Not all relays connected + Es sind nicht alle Relais verbunden + alert title + Not compatible! Nicht kompatibel! @@ -5703,7 +6237,7 @@ Das ist Ihr Link für die Gruppe %@! OK OK - No comment provided by engineer. + alert button Off @@ -5722,11 +6256,21 @@ new chat action Alte Datenbank No comment provided by engineer. + + On your phone, not on servers. + Auf Ihrem Gerät, nicht auf Servern. + No comment provided by engineer. + One-time invitation link Einmal-Einladungslink No comment provided by engineer. + + One-time link + Einmal-Link + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5746,9 +6290,14 @@ Dies erfordert die Aktivierung eines VPNs. Onion-Hosts werden nicht verwendet. No comment provided by engineer. + + Only channel owners can change channel preferences. + Kanal-Präferenzen können nur von Kanal-Eigentümern geändert werden. + No comment provided by engineer. + Only chat owners can change preferences. - Nur Chat-Eigentümer können die Präferenzen ändern. + Präferenzen können nur von Chat-Eigentümern geändert werden. No comment provided by engineer. @@ -5849,7 +6398,8 @@ Dies erfordert die Aktivierung eines VPNs. Open Öffnen - alert action + alert action +alert button Open Settings @@ -5861,6 +6411,11 @@ Dies erfordert die Aktivierung eines VPNs. Änderungen öffnen No comment provided by engineer. + + Open channel + Kanal öffnen + new chat action + Open chat Chat öffnen @@ -5881,6 +6436,11 @@ Dies erfordert die Aktivierung eines VPNs. Nutzungsbedingungen öffnen No comment provided by engineer. + + Open external link? + Externen Link öffnen? + alert title + Open full link Vollständigen Link öffnen @@ -5901,6 +6461,11 @@ Dies erfordert die Aktivierung eines VPNs. Migration auf ein anderes Gerät öffnen authentication reason + + Open new channel + Neuen Kanal öffnen + new chat action + Open new chat Neuen Chat öffnen @@ -5946,6 +6511,17 @@ Dies erfordert die Aktivierung eines VPNs. Betreiber-Server alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + Betreiber verpflichten sich: +- Unabhängig zu bleiben +- Metadaten auf ein Minimum zu reduzieren +- Geprüften Open‑Source‑Code einzusetzen + No comment provided by engineer. + Or import archive file Oder importieren Sie eine Archiv-Datei @@ -5966,6 +6542,11 @@ Dies erfordert die Aktivierung eines VPNs. Oder teilen Sie diesen Datei-Link sicher No comment provided by engineer. + + Or show QR in person or via video call. + Oder den QR‑Code persönlich oder per Videoanruf zeigen. + No comment provided by engineer. + Or show this code Oder diesen QR-Code anzeigen @@ -5976,6 +6557,11 @@ Dies erfordert die Aktivierung eines VPNs. Oder zum privaten Teilen No comment provided by engineer. + + Or use this QR - print or show online. + Oder diesen QR‑Code verwenden – ausgedruckt oder online. + No comment provided by engineer. + Organize chats into lists Chats in Listen verwalten @@ -5993,6 +6579,21 @@ Dies erfordert die Aktivierung eines VPNs. %@ alert message + + Owner + Eigentümer + No comment provided by engineer. + + + Owners + Eigentümer + No comment provided by engineer. + + + Ownership: you can run your own relays. + Volle Kontrolle: Sie können Ihre eigenen Relais betreiben. + No comment provided by engineer. + PING count PING-Zähler @@ -6048,6 +6649,11 @@ Dies erfordert die Aktivierung eines VPNs. Bild einfügen No comment provided by engineer. + + Paste link / Scan + Link einfügen / Scannen + No comment provided by engineer. + Paste link to connect! Zum Verbinden den Link einfügen! @@ -6202,6 +6808,16 @@ Fehler: %@ Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren. No comment provided by engineer. + + Preset relay address + Voreingestellte Relais-Adresse + No comment provided by engineer. + + + Preset relay name + Voreingestellter Relais-Name + No comment provided by engineer. + Preset server address Voreingestellte Serveradresse @@ -6237,14 +6853,14 @@ Fehler: %@ Datenschutz- und Nutzungsbedingungen. No comment provided by engineer. - - Privacy redefined - Datenschutz neu definiert + + Privacy: for owners and subscribers. + Privatsphäre: für Besitzer und Abonnenten. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. - Private Chats, Gruppen und Ihre Kontakte sind für Server-Betreiber nicht zugänglich. + + Private and secure messaging. + Private und sichere Kommunikation. No comment provided by engineer. @@ -6287,6 +6903,11 @@ Fehler: %@ Zeitüberschreitung der privaten Routing-Sitzung alert title + + Proceed + Fortfahren + alert action + Profile and server connections Profil und Serververbindungen @@ -6312,9 +6933,9 @@ Fehler: %@ Profil-Design No comment provided by engineer. - - Profile update will be sent to your contacts. - Profil-Aktualisierung wird an Ihre Kontakte gesendet. + + Profile update will be sent to your SimpleX contacts. + Profil-Aktualisierung wird an Ihre SimpleX-Kontakte gesendet. alert message @@ -6322,6 +6943,11 @@ Fehler: %@ Audio-/Video-Anrufe nicht erlauben. No comment provided by engineer. + + Prohibit chats with admins. + Chat mit Administratoren nicht erlauben. + No comment provided by engineer. + Prohibit irreversible message deletion. Unwiederbringliches löschen von Nachrichten nicht erlauben. @@ -6352,6 +6978,11 @@ Fehler: %@ Das Senden von Direktnachrichten an Gruppenmitglieder nicht erlauben. No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + Das Senden von Direktnachrichten an Abonnenten nicht erlauben. + No comment provided by engineer. + Prohibit sending disappearing messages. Das Senden von verschwindenden Nachrichten nicht erlauben. @@ -6419,6 +7050,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Der Proxy benötigt ein Passwort No comment provided by engineer. + + Public channels - speak freely 🚀 + Öffentliche Kanäle – frei sprechen 🚀 + No comment provided by engineer. + Push notifications Push-Benachrichtigungen @@ -6459,24 +7095,14 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Mehr erfahren No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - Lesen Sie mehr dazu im [Benutzerhandbuch](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + Lesen Sie mehr dazu im Benutzerhandbuch. No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) lesen. - No comment provided by engineer. - - - Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/readme.html#connect-to-friends) lesen. - No comment provided by engineer. - - - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). - Erfahren Sie in unserem [GitHub-Repository](https://github.com/simplex-chat/simplex-chat#readme) mehr dazu. + + Read more in our GitHub repository. + Erfahren Sie in unserem GitHub-Repository mehr dazu. No comment provided by engineer. @@ -6636,6 +7262,31 @@ swipe action Mitglied ablehnen? alert title + + Relay + Relais + No comment provided by engineer. + + + Relay address + Relais-Adresse + alert title + + + Relay connection failed + Relais-Verbindung fehlgeschlagen + alert title + + + Relay link + Relais-Link + No comment provided by engineer. + + + Relay results: + Relay‑Status: + alert message + Relay server is only used if necessary. Another party can observe your IP address. Relais-Server werden nur genutzt, wenn sie benötigt werden. Ihre IP-Adresse kann von Anderen erfasst werden. @@ -6646,6 +7297,16 @@ swipe action Relais-Server schützen Ihre IP-Adresse, aber sie können die Anrufdauer erfassen. No comment provided by engineer. + + Relay test failed! + Relais-Test fehlgeschlagen! + No comment provided by engineer. + + + Reliability: many relays per channel. + Zuverlässigkeit: mehrere Relais pro Kanal. + No comment provided by engineer. + Remove Entfernen @@ -6686,6 +7347,16 @@ swipe action Passwort aus dem Schlüsselbund entfernen? No comment provided by engineer. + + Remove subscriber + Abonnent entfernen + No comment provided by engineer. + + + Remove subscriber? + Abonnent entfernen? + alert title + Removes messages and blocks members. Entfernt Nachrichten und blockiert Mitglieder. @@ -6921,6 +7592,11 @@ swipe action SOCKS-Proxy No comment provided by engineer. + + Safe web links + Sichere Web-Links + No comment provided by engineer. + Safely receive files Dateien sicher herunterladen @@ -6947,6 +7623,11 @@ chat item action Speichern (und Mitglieder benachrichtigen) alert button + + Save (and notify subscribers) + Speichern (Abonnenten benachrichtigen) + alert button + Save admission settings? Speichern der Aufnahme-Einstellungen? @@ -6962,6 +7643,11 @@ chat item action Speichern und Gruppenmitglieder benachrichtigen No comment provided by engineer. + + Save and notify subscribers + Speichern und Abonnenten benachrichtigen + No comment provided by engineer. + Save and reconnect Speichern und neu verbinden @@ -6972,6 +7658,16 @@ chat item action Gruppen-Profil sichern und aktualisieren No comment provided by engineer. + + Save channel profile + Kanalprofil speichern + No comment provided by engineer. + + + Save channel profile? + Kanalprofil speichern? + alert title + Save group profile Gruppenprofil speichern @@ -7152,6 +7848,11 @@ chat item action Sicherheitscode No comment provided by engineer. + + Security: owners hold channel keys. + Sicherheit: Eigentümer besitzen die Kanalschlüssel. + No comment provided by engineer. + Select Auswählen @@ -7169,7 +7870,7 @@ chat item action Selected chat preferences prohibit this message. - Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt. + Diese Nachricht ist wegen der gewählten Chat-Präferenzen nicht erlaubt. No comment provided by engineer. @@ -7229,7 +7930,7 @@ chat item action Send link previews - Link-Vorschau senden + Linkvorschau senden No comment provided by engineer. @@ -7282,6 +7983,11 @@ chat item action Anfrage ohne Nachricht senden No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + Den Link über einen beliebigen Messenger versenden – es ist sicher. Bitte in SimpleX einfügen. + No comment provided by engineer. + Send them from gallery or custom keyboards. Senden Sie diese aus dem Fotoalbum oder von individuellen Tastaturen. @@ -7292,6 +7998,11 @@ chat item action Bis zu 100 der letzten Nachrichten an neue Gruppenmitglieder senden. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + Bis zu 100 der letzten Nachrichten an neue Abonnenten senden. + No comment provided by engineer. + Send your private feedback to groups. Senden Sie Ihr privates Feedback an Gruppen. @@ -7307,6 +8018,11 @@ chat item action Der Absender hat möglicherweise die Verbindungsanfrage gelöscht. No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + Das Senden einer Link-Vorschau kann Ihre IP‑Adresse an die Website übermitteln. Sie können dies später in den Datenschutzeinstellungen ändern. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. Das Senden von Empfangsbestätigungen an alle Kontakte in allen sichtbaren Chat-Profilen wird aktiviert. @@ -7432,6 +8148,11 @@ chat item action Das Server-Protokoll wurde geändert. alert title + + Server requires authorization to connect to relay, check password. + Der Server erfordert eine Autorisierung, um eine Verbindung zum Relais herzustellen. Bitte Passwort überprüfen. + relay test error + Server requires authorization to create queues, check password. Der Server erfordert zum Erstellen von Warteschlangen eine Autorisierung. Bitte überprüfen Sie das Passwort. @@ -7562,6 +8283,16 @@ chat item action Die Einstellungen wurden geändert. alert message + + Setup notifications + Benachrichtigungen einrichten + No comment provided by engineer. + + + Setup routers + Router einrichten + No comment provided by engineer. + Shape profile images Form der Profil-Bilder @@ -7598,11 +8329,16 @@ chat item action Die Adresse öffentlich teilen No comment provided by engineer. - - Share address with contacts? - Die Adresse mit Kontakten teilen? + + Share address with SimpleX contacts? + Die Adresse mit SimpleX-Kontakten teilen? alert title + + Share channel + Kanal teilen + No comment provided by engineer. + Share from other apps. Aus anderen Apps heraus teilen. @@ -7628,6 +8364,11 @@ chat item action Profil teilen No comment provided by engineer. + + Share relay address + Relais-Adresse teilen + No comment provided by engineer. + Share this 1-time invite link Teilen Sie diesen Einmal-Einladungslink @@ -7638,9 +8379,14 @@ chat item action Mit SimpleX teilen No comment provided by engineer. - - Share with contacts - Mit Kontakten teilen + + Share via chat + Per Chat teilen + No comment provided by engineer. + + + Share with SimpleX contacts + Mit SimpleX-Kontakten teilen No comment provided by engineer. @@ -7770,7 +8516,7 @@ chat item action SimpleX channel link - SimpleX-Kanal-Link + SimpleX-Kanallink simplex link type @@ -7813,9 +8559,9 @@ chat item action Die SimpleX-Protokolle wurden von Trail of Bits überprüft. No comment provided by engineer. - - SimpleX relay link - SimpleX Relais-Link + + SimpleX relay address + SimpleX Relais-Adresse simplex link type @@ -7891,6 +8637,11 @@ report reason Quadratisch, kreisförmig oder irgendetwas dazwischen. No comment provided by engineer. + + Star on GitHub + Stern auf GitHub vergeben + No comment provided by engineer. + Start chat Starten Sie den Chat @@ -7991,6 +8742,78 @@ report reason Abonniert No comment provided by engineer. + + Subscriber + Abonnent + No comment provided by engineer. + + + Subscriber reports + Abonnenten-Meldungen + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + Abonnent wird aus dem Kanal entfernt. Dies kann nicht rückgängig gemacht werden! + alert message + + + Subscribers + Abonnenten + No comment provided by engineer. + + + Subscribers can add message reactions. + Abonnenten können eine Reaktion auf Nachrichten geben. + No comment provided by engineer. + + + Subscribers can chat with admins. + Abonnenten können mit Administratoren chatten. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + Abonnenten können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + Abonnenten können Nachrichten an Moderatoren melden. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + Abonnenten können SimpleX-Links versenden. + No comment provided by engineer. + + + Subscribers can send direct messages. + Abonnenten können Direktnachrichten versenden. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + Abonnenten können verschwindende Nachrichten versenden. + No comment provided by engineer. + + + Subscribers can send files and media. + Abonnenten können Dateien und Medien versenden. + No comment provided by engineer. + + + Subscribers can send voice messages. + Abonnenten können Sprachnachrichten versenden. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + Abonnenten verbinden sich über den Relais‑Link mit dem Kanal. +Die Relais-Adresse wurde zur Einrichtung dieses Relais für diesen Kanal verwendet. + No comment provided by engineer. + Subscription errors Fehler beim Abonnieren @@ -8071,6 +8894,11 @@ report reason Machen Sie ein Foto No comment provided by engineer. + + Talk to someone + Mit jemandem sprechen + No comment provided by engineer. + Tap Connect to chat Verbinden tippen, um zu chatten @@ -8086,9 +8914,9 @@ report reason Verbinden tippen, um den Bot zu nutzen. No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. - Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen. + + Tap Join channel + Tippen, um dem Kanal beizutreten No comment provided by engineer. @@ -8108,7 +8936,7 @@ report reason Tap to activate profile. - Zum Aktivieren des Profils tippen. + Tippen, um das Profil zu aktivieren. No comment provided by engineer. @@ -8121,9 +8949,14 @@ report reason Zum Inkognito beitreten tippen No comment provided by engineer. + + Tap to open + Zum Öffnen tippen + No comment provided by engineer. + Tap to paste link - Zum Link einfügen tippen + Tippen, um den Link einzufügen No comment provided by engineer. @@ -8139,13 +8972,19 @@ report reason Test failed at step %@. Der Test ist beim Schritt %@ fehlgeschlagen. - server test failure + relay test failure +server test failure Test notifications Benachrichtigungen testen No comment provided by engineer. + + Test relay + Relais testen + No comment provided by engineer. + Test server Teste Server @@ -8198,6 +9037,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Durch Verwendung verschiedener Netzwerk-Betreiber für jede Unterhaltung schützt die App Ihre Privatsphäre. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + Die App hat diese Nachricht nach %lld Empfangsversuchen entfernt. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Die App wird eine Bestätigung bei Downloads von unbekannten Datei-Servern anfordern (außer bei .onion). @@ -8213,6 +9057,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Der von Ihnen gescannte Code ist kein SimpleX-Link-QR-Code. No comment provided by engineer. + + The connection reached the limit of undelivered messages + Die Verbindung hat das Limit für nicht zugestellte Nachrichten erreicht + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. Diese Verbindung hat das Limit der nicht ausgelieferten Nachrichten erreicht. Ihr Kontakt ist möglicherweise offline. @@ -8238,9 +9087,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Die Verschlüsselung funktioniert und ein neues Verschlüsselungsabkommen ist nicht erforderlich. Es kann zu Verbindungsfehlern kommen! No comment provided by engineer. - - The future of messaging - Die nächste Generation von privatem Messaging + + The first network where you own +your contacts and groups. + Das erste Netzwerk, +in dem Sie Ihre Kontakte und Gruppen besitzen. No comment provided by engineer. @@ -8278,6 +9129,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Die alte Datenbank wurde während der Migration nicht entfernt. Sie kann gelöscht werden. No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + Die älteste Freiheit des Menschen - mit einem anderen Menschen sprechen zu können, ohne beobachtet zu werden - gestützt auf einer Infrastruktur, die Sie nicht verraten kann. + No comment provided by engineer. + The same conditions will apply to operator **%@**. Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**. @@ -8323,6 +9179,16 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Design No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + Dann sind wir online gegangen, und jede Plattform wollte Etwas von Ihnen - Ihren Namen, Ihre Nummer, Ihre Freunde. Wir akzeptierten, dass es der Preis mit Anderen zu kommunizieren ist, Jemandem preiszugeben, mit wem und wie wir miteinander kommunizieren. Jede Generation, Menschen und Technologien, kannten es nur so - Telefon, E-Mail, Messenger, soziale Medien. Es schien der einzig mögliche Weg zu sein. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + Es gibt einen anderen Weg. Ein Netzwerk ohne Telefonnummern, ohne Benutzernamen, ohne Benutzerkennungen und ohne jegliche Benutzeridentität. Ein Netzwerk, welches Menschen verbindet und verschlüsselte Nachrichten überträgt, ohne zu wissen, wer mit wem verbunden ist. + No comment provided by engineer. + These conditions will also apply for: **%@**. Diese Nutzungsbedingungen gelten auch für: **%@**. @@ -8388,6 +9254,16 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Diese Gruppe existiert nicht mehr. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + Dies ist eine Chat‑Relais-Adresse, welche nicht zum Verbinden verwendet werden kann. + alert message + + + This is your link for channel %@! + Dies ist Ihr Link für den Kanal %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Für diesen Link wird eine neuere App-Version benötigt. Bitte aktualisieren Sie die App oder bitten Sie Ihren Kontakt einen kompatiblen Link zu senden. @@ -8438,6 +9314,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Um unerwünschte Nachrichten zu verbergen. No comment provided by engineer. + + To make SimpleX Network last. + Für ein dauerhaftes SimpleX-Netzwerk. + No comment provided by engineer. + To make a new connection Um eine Verbindung mit einem neuen Kontakt zu erstellen @@ -8525,11 +9406,6 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen. No comment provided by engineer. - - Toggle chat list: - Chat-Liste umschalten: - No comment provided by engineer. - Toggle incognito when connecting. Inkognito beim Verbinden einschalten. @@ -8545,6 +9421,11 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Deckkraft der Symbolleiste No comment provided by engineer. + + Top bar + Obere Leiste + No comment provided by engineer. + Total Summe aller Abonnements @@ -8610,6 +9491,11 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Mitglied freigeben? No comment provided by engineer. + + Unblock subscriber for all? + Abonnent für alle freigeben? + No comment provided by engineer. + Undelivered messages Nicht ausgelieferte Nachrichten @@ -8710,13 +9596,18 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Unsupported connection link Verbindungs-Link wird nicht unterstützt - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. Bis zu 100 der letzten Nachrichten werden an neue Mitglieder gesendet. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + Bis zu 100 der letzten Nachrichten werden an neue Abonnenten gesendet. + No comment provided by engineer. + Update Aktualisieren @@ -8842,11 +9733,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s TCP-Port 443 nur für voreingestellte Server verwenden. No comment provided by engineer. - - Use chat - Verwenden Sie Chat - No comment provided by engineer. - Use current profile Aktuelles Profil nutzen @@ -8862,6 +9748,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Für Nachrichten verwenden No comment provided by engineer. + + Use for new channels + Für neue Kanäle verwenden + No comment provided by engineer. + Use for new connections Für neue Verbindungen nutzen @@ -8902,6 +9793,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Sie nutzen privates Routing mit unbekannten Servern. No comment provided by engineer. + + Use relay + Relais verwenden + No comment provided by engineer. + Use server Server nutzen @@ -8922,6 +9818,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Die App mit einer Hand bedienen. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + Diese Adresse in Ihrem Social‑Media‑Profil, auf Ihrer Webseite oder in Ihrer E‑Mail‑Signatur verwenden. + No comment provided by engineer. + Use web port Web-Port nutzen @@ -8942,6 +9843,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Verwendung von SimpleX-Chat-Servern. No comment provided by engineer. + + Verify + Überprüfen + relay test step + Verify code with desktop Code mit dem Desktop überprüfen @@ -9062,6 +9968,21 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Sprachnachrichten… No comment provided by engineer. + + Wait + Abwarten + alert action + + + Wait response + Antwort abwarten + relay test step + + + Waiting for channel owner to add relays. + Warte auf das Hinzufügen von Relais durch den Eigentümer des Kanals. + No comment provided by engineer. + Waiting for desktop... Es wird auf den Desktop gewartet... @@ -9102,6 +10023,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Warnung: Sie könnten einige Daten verlieren! No comment provided by engineer. + + We made connecting simpler for new users. + Wir haben das Verbinden für neue Nutzer vereinfacht. + No comment provided by engineer. + WebRTC ICE servers WebRTC ICE-Server @@ -9152,6 +10078,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil auch für die Gruppen verwendet, für die Sie von diesem Kontakt eingeladen werden. No comment provided by engineer. + + Why SimpleX is built. + Warum SimpleX entwickelt wurde. + No comment provided by engineer. + WiFi WiFi @@ -9364,6 +10295,11 @@ Verbindungsanfrage wiederholen? Über die Geräte-Einstellungen können Sie die Benachrichtigungsvorschau im Sperrbildschirm erlauben. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + Sie können einen Link oder QR-Code teilen - damit kann jeder dem Kanal beitreten. + 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. 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. @@ -9409,16 +10345,25 @@ Verbindungsanfrage wiederholen? Sie können keine Nachrichten versenden! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + Sie verpflichten sich dazu: +- nur legale Inhalte in öffentlichen Gruppen zu versenden +- andere Nutzer zu respektieren - kein Spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + Sie haben sich über diesen Relais‑Link mit dem Kanal verbunden. + No comment provided by engineer. + You could not be verified; please try again. Sie konnten nicht überprüft werden; bitte versuchen Sie es erneut. No comment provided by engineer. - - You decide who can connect. - Sie entscheiden, wer sich mit Ihnen verbinden kann. - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -9486,6 +10431,11 @@ Verbindungsanfrage wiederholen? Sie sollten Benachrichtigungen erhalten. token info + + You were born without an account + Sie wurden ohne eine Benutzerkennung geboren. + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. Sie können erst dann Nachrichten versenden, **sobald Ihre Anfrage angenommen wurde**. @@ -9521,6 +10471,11 @@ Verbindungsanfrage wiederholen? Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + Sie werden keine Nachrichten mehr aus diesem Kanal erhalten. Der Chatverlauf bleibt erhalten. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf bleibt erhalten. @@ -9566,6 +10521,11 @@ Verbindungsanfrage wiederholen? Anrufe No comment provided by engineer. + + Your channel + Ihr Kanal + No comment provided by engineer. + Your chat database Chat-Datenbank @@ -9616,6 +10576,11 @@ Verbindungsanfrage wiederholen? Ihre Kontakte bleiben weiterhin verbunden. No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + Ihre Kommunikation gehört Ihnen, so wie es immer war, bevor es das Internet gab. Das Netzwerk ist kein Ort, den Sie besuchen. Es ist ein Ort, den Sie erschaffen und besitzen und Niemand kann es Ihnen nehmen, egal ob Sie es privat oder öffentlich machen. + No comment provided by engineer. + Your credentials may be sent unencrypted. Ihre Anmeldeinformationen können unverschlüsselt versendet werden. @@ -9636,6 +10601,11 @@ Verbindungsanfrage wiederholen? Ihre Gruppe No comment provided by engineer. + + Your network + Ihr Netzwerk + No comment provided by engineer. + Your preferences Ihre Präferenzen @@ -9651,6 +10621,13 @@ Verbindungsanfrage wiederholen? Mein Profil No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + Ihr Profil **%@** wird mit Kanal‑Relais und Abonnenten geteilt. +Relais können auf Kanalnachrichten zugreifen. + No comment provided by engineer. + Your profile **%@** will be shared. Ihr Profil **%@** wird geteilt. @@ -9671,11 +10648,26 @@ Verbindungsanfrage wiederholen? Ihr Profil wurde geändert. Wenn Sie es speichern, wird das aktualisierte Profil an alle Ihre Kontakte gesendet. alert message + + Your public address + Ihre öffentliche Adresse + No comment provided by engineer. + Your random profile Ihr Zufallsprofil No comment provided by engineer. + + Your relay address + Ihre Relais-Adresse + No comment provided by engineer. + + + Your relay name + Ihr Relais-Name + No comment provided by engineer. + Your server address Ihre Serveradresse @@ -9691,21 +10683,11 @@ Verbindungsanfrage wiederholen? Einstellungen No comment provided by engineer. - - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - [Unterstützen Sie uns](https://github.com/simplex-chat/simplex-chat#contribute) - No comment provided by engineer. - [Send us email](mailto:chat@simplex.chat) [Senden Sie uns eine E-Mail](mailto:chat@simplex.chat) No comment provided by engineer. - - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - [Stern auf GitHub vergeben](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - \_italic_ \_kursiv_ @@ -9721,6 +10703,11 @@ Verbindungsanfrage wiederholen? Danach die gewünschte Aktion auswählen: No comment provided by engineer. + + accepted + Angenommen + No comment provided by engineer. + accepted %@ %@ angenommen @@ -9741,6 +10728,11 @@ Verbindungsanfrage wiederholen? hat Sie angenommen rcv group event chat item + + active + Aktiv + No comment provided by engineer. + admin Admin @@ -9852,6 +10844,11 @@ marked deleted chat item preview text Anrufen… call status + + can't broadcast + Broadcast nicht möglich + No comment provided by engineer. + can't send messages Es können keine Nachrichten gesendet werden @@ -9887,6 +10884,16 @@ marked deleted chat item preview text Wechsel der Empfängeradresse wurde gestartet… chat item text + + channel + Kanal + shown as sender role for channel messages + + + channel profile updated + Kanalprofil wurde aktualisiert + snd group event chat item + colored farbig @@ -9909,7 +10916,7 @@ marked deleted chat item preview text connecting - verbinde + Verbinde No comment provided by engineer. @@ -9929,7 +10936,7 @@ marked deleted chat item preview text connecting (introduction invitation) - Verbinde (nach einer Einladung) + Verbindung (nach einer Einladung) No comment provided by engineer. @@ -10033,6 +11040,11 @@ pref value Gelöscht deleted chat item + + deleted channel + Kanal gelöscht + rcv group event chat item + deleted contact Gelöschter Kontakt @@ -10055,7 +11067,7 @@ pref value disabled - deaktiviert + Deaktiviert No comment provided by engineer. @@ -10143,6 +11155,11 @@ pref value Fehler No comment provided by engineer. + + error: %@ + Fehler: %@ + receive error chat item + expired Abgelaufen @@ -10150,6 +11167,7 @@ pref value failed + Fehlgeschlagen No comment provided by engineer. @@ -10272,6 +11290,11 @@ pref value hat die Gruppe verlassen rcv group event chat item + + link + Link + No comment provided by engineer. + marked deleted als gelöscht markiert @@ -10289,7 +11312,7 @@ pref value connected - ist der Gruppe beigetreten + Verbunden rcv group event chat item @@ -10342,6 +11365,11 @@ pref value nie delete after time + + new + Neu + No comment provided by engineer. + new message Neue Nachricht @@ -10465,6 +11493,11 @@ time to disappear Abgelehnter Anruf call status + + relay + Relais + member role + removed entfernt @@ -10475,6 +11508,16 @@ time to disappear hat %@ aus der Gruppe entfernt rcv group event chat item + + removed (%d attempts) + Entfernt (%d Versuche) + receive error chat item + + + removed by operator + Vom Betreiber entfernt + No comment provided by engineer. + removed contact address Die Kontaktadresse wurde entfernt @@ -10629,6 +11672,11 @@ Zuletzt empfangene Nachricht: %2$@ Ungeschützt No comment provided by engineer. + + updated channel profile + Kanalprofil aktualisiert + rcv group event chat item + updated group profile Aktualisiertes Gruppenprofil @@ -10649,6 +11697,11 @@ Zuletzt empfangene Nachricht: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + via %@ + relay hostname + via contact address link über einen Kontaktadressen-Link @@ -10724,6 +11777,11 @@ Zuletzt empfangene Nachricht: %2$@ Sie sind Beobachter No comment provided by engineer. + + you are subscriber + Sie sind Abonnent + No comment provided by engineer. + you blocked %@ Sie haben %@ blockiert @@ -10784,6 +11842,11 @@ Zuletzt empfangene Nachricht: %2$@ \~durchstreichen~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + ⚠️ Signaturüberprüfung fehlgeschlagen: %@. + owner verification + @@ -11041,7 +12104,7 @@ Zuletzt empfangene Nachricht: %2$@ Selected chat preferences prohibit this message. - Die gewählten Chat-Einstellungen erlauben diese Nachricht nicht. + Diese Nachricht ist wegen der gewählten Chat-Präferenzen nicht erlaubt. No comment provided by engineer. 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 fbc3b2dfa8..5e95cf39cc 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -185,6 +185,24 @@ %d months time interval + + %d relays failed + %d relays failed + channel relay bar +channel subscriber relay bar + + + %d relays not active + %d relays not active + channel relay bar +channel subscriber relay bar + + + %d relays removed + %d relays removed + channel relay bar +channel subscriber relay bar + %d sec %d sec @@ -200,11 +218,63 @@ %d skipped message(s) integrity error chat item + + %d subscriber + %d subscriber + channel subscriber count + + + %d subscribers + %d subscribers + channel subscriber count + %d weeks %d weeks time interval + + %1$d/%2$d relays active + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + %1$d/%2$d relays active, %3$d errors + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + %1$d/%2$d relays active, %3$d removed + channel relay bar + + + %1$d/%2$d relays connected + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + %1$d/%2$d relays connected, %3$d failed + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + %1$d/%2$d relays connected, %3$d removed + channel subscriber relay bar + %lld %lld @@ -215,6 +285,11 @@ %lld %@ No comment provided by engineer. + + %lld channel events + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld contact(s) selected @@ -315,11 +390,21 @@ %u messages skipped. No comment provided by engineer. + + (from owner) + (from owner) + chat link info line + (new) (new) No comment provided by engineer. + + (signed) + (signed) + chat link info line + (this device v%@) (this device v%@) @@ -365,6 +450,11 @@ **Scan / Paste link**: to connect via a link you received. No comment provided by engineer. + + **Test relay** to retrieve its name. + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Warning**: Instant push notifications require passphrase saved in Keychain. @@ -408,6 +498,15 @@ - and more! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -506,6 +605,11 @@ time interval A few more things No comment provided by engineer. + + A link for one person to connect + A link for one person to connect + No comment provided by engineer. + A new contact A new contact @@ -632,9 +736,9 @@ swipe action Active connections 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. - Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. @@ -702,6 +806,11 @@ swipe action Added message servers No comment provided by engineer. + + Adding relays will be supported later. + Adding relays will be supported later. + No comment provided by engineer. + Additional accent Additional accent @@ -822,6 +931,16 @@ swipe action All profiles profile dropdown + + All relays failed + All relays failed + No comment provided by engineer. + + + All relays removed + All relays removed + No comment provided by engineer. + All reports will be archived for you. All reports will be archived for you. @@ -882,6 +1001,11 @@ swipe action Allow irreversible message deletion only if your contact allows it to you. (24 hours) No comment provided by engineer. + + Allow members to chat with admins. + Allow members to chat with admins. + No comment provided by engineer. + Allow message reactions only if your contact allows them. Allow message reactions only if your contact allows them. @@ -897,6 +1021,11 @@ swipe action Allow sending direct messages to members. No comment provided by engineer. + + Allow sending direct messages to subscribers. + Allow sending direct messages to subscribers. + No comment provided by engineer. + Allow sending disappearing messages. Allow sending disappearing messages. @@ -907,6 +1036,11 @@ swipe action Allow sharing No comment provided by engineer. + + Allow subscribers to chat with admins. + Allow subscribers to chat with admins. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Allow to irreversibly delete sent messages. (24 hours) @@ -1012,11 +1146,6 @@ swipe action Answer call No comment provided by engineer. - - Anybody can host servers. - Anybody can host servers. - No comment provided by engineer. - App build: %@ App build: %@ @@ -1222,6 +1351,23 @@ swipe action Bad message hash No comment provided by engineer. + + Be free +in your network + Be free +in your network + No comment provided by engineer. + + + Be free in your network. + Be free in your network. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + Because we destroyed the power to know who you are. So that your power can never be taken. + No comment provided by engineer. + Better calls Better calls @@ -1317,6 +1463,11 @@ swipe action Block member? No comment provided by engineer. + + Block subscriber for all? + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Blocked by admin @@ -1367,6 +1518,16 @@ swipe action Both you and your contact can send voice messages. No comment provided by engineer. + + Bottom bar + Bottom bar + No comment provided by engineer. + + + Broadcast + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1375,7 +1536,7 @@ swipe action Business address Business address - No comment provided by engineer. + chat link info line Business chats @@ -1397,15 +1558,6 @@ swipe action By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - No comment provided by engineer. - Call already ended! Call already ended! @@ -1554,6 +1706,82 @@ new chat action authentication reason set passcode view + + Channel + Channel + No comment provided by engineer. + + + Channel display name + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + Channel full name (optional) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + Channel has no active relays. Please try to join later. + alert message +alert subtitle + + + Channel image + Channel image + No comment provided by engineer. + + + Channel link + Channel link + chat link info line + + + Channel preferences + Channel preferences + No comment provided by engineer. + + + Channel profile + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel temporarily unavailable + Channel temporarily unavailable + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + + + Channels + Channels + No comment provided by engineer. + Chat Chat @@ -1639,6 +1867,26 @@ set passcode view Chat profile No comment provided by engineer. + + Chat relay + Chat relay + No comment provided by engineer. + + + Chat relays + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Chat theme @@ -1657,7 +1905,8 @@ set passcode view Chat with admins Chat with admins - chat toolbar + chat feature +chat toolbar Chat with member @@ -1674,11 +1923,26 @@ set passcode view Chats No comment provided by engineer. + + Chats with admins are prohibited. + Chats with admins are prohibited. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + alert message + Chats with members Chats with members No comment provided by engineer. + + Chats with members are disabled + Chats with members are disabled + No comment provided by engineer. + Check messages every 20 min. Check messages every 20 min. @@ -1689,6 +1953,16 @@ set passcode view Check messages when allowed. No comment provided by engineer. + + Check relay address and try again. + Check relay address and try again. + alert message + + + Check relay name and try again. + Check relay name and try again. + alert message + Check server address and try again. Check server address and try again. @@ -1834,9 +2108,9 @@ set passcode view Configure ICE servers No comment provided by engineer. - - Configure server operators - Configure server operators + + Configure relays + Configure relays No comment provided by engineer. @@ -1897,7 +2171,8 @@ set passcode view Connect Connect - server test step + relay test step +server test step Connect automatically @@ -1943,6 +2218,11 @@ This is your own one-time link! Connect via link new chat sheet title + + Connect via link or QR code + Connect via link or QR code + No comment provided by engineer. + Connect via one-time link Connect via one-time link @@ -2021,7 +2301,7 @@ This is your own one-time link! Connection error (AUTH) Connection error (AUTH) - No comment provided by engineer. + conn error description Connection failed @@ -2080,6 +2360,11 @@ This is your own one-time link! Connections No comment provided by engineer. + + Contact address + Contact address + chat link info line + Contact allows Contact allows @@ -2150,6 +2435,11 @@ This is your own one-time link! Continue No comment provided by engineer. + + Contribute + Contribute + No comment provided by engineer. + Conversation deleted! Conversation deleted! @@ -2178,12 +2468,7 @@ This is your own one-time link! Correct name to %@? Correct name to %@? - No comment provided by engineer. - - - Create - Create - No comment provided by engineer. + alert message Create 1-time link @@ -2235,6 +2520,16 @@ This is your own one-time link! Create profile No comment provided by engineer. + + Create public channel + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + Create public channel (BETA) + No comment provided by engineer. + Create queue Create queue @@ -2245,11 +2540,21 @@ This is your own one-time link! Create your address No comment provided by engineer. + + Create your link + Create your link + No comment provided by engineer. + Create your profile Create your profile No comment provided by engineer. + + Create your public address + Create your public address + No comment provided by engineer. + Created Created @@ -2270,6 +2575,11 @@ This is your own one-time link! Creating archive link No comment provided by engineer. + + Creating channel + Creating channel + No comment provided by engineer. + Creating link… Creating link… @@ -2428,10 +2738,10 @@ This is your own one-time link! Debug delivery No comment provided by engineer. - - Decentralized - Decentralized - No comment provided by engineer. + + Decode link + Decode link + relay test step Decryption error @@ -2479,6 +2789,16 @@ swipe action Delete and notify contact No comment provided by engineer. + + Delete channel + Delete channel + No comment provided by engineer. + + + Delete channel? + Delete channel? + No comment provided by engineer. + Delete chat Delete chat @@ -2650,6 +2970,11 @@ alert button Delete queue server test step + + Delete relay + Delete relay + No comment provided by engineer. + Delete report Delete report @@ -2815,6 +3140,16 @@ alert button Direct messages between members are prohibited. No comment provided by engineer. + + Direct messages between subscribers are prohibited. + Direct messages between subscribers are prohibited. + No comment provided by engineer. + + + Disable + Disable + alert button + Disable (keep overrides) Disable (keep overrides) @@ -2920,6 +3255,11 @@ alert button Do not send history to new members. No comment provided by engineer. + + Do not send history to new subscribers. + Do not send history to new subscribers. + No comment provided by engineer. + Do not use credentials with proxy. Do not use credentials with proxy. @@ -3021,11 +3361,21 @@ chat item action E2E encrypted notifications. No comment provided by engineer. + + Easier to invite your friends 👋 + Easier to invite your friends 👋 + No comment provided by engineer. + Edit Edit chat item action + + Edit channel profile + Edit channel profile + No comment provided by engineer. + Edit group profile Edit group profile @@ -3039,7 +3389,7 @@ chat item action Enable Enable - No comment provided by engineer. + alert button Enable (keep overrides) @@ -3061,6 +3411,11 @@ chat item action Enable TCP keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Enable automatic message deletion? @@ -3071,6 +3426,11 @@ chat item action Enable camera access No comment provided by engineer. + + Enable chats with admins? + Enable chats with admins? + alert title + Enable disappearing messages by default. Enable disappearing messages by default. @@ -3091,16 +3451,16 @@ chat item action Enable instant notifications? No comment provided by engineer. + + Enable link previews? + Enable link previews? + alert title + Enable lock Enable lock No comment provided by engineer. - - Enable notifications - Enable notifications - No comment provided by engineer. - Enable periodic notifications? Enable periodic notifications? @@ -3206,6 +3566,11 @@ chat item action Enter Passcode No comment provided by engineer. + + Enter channel name… + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Enter correct passphrase. @@ -3231,6 +3596,16 @@ chat item action Enter password above to show! No comment provided by engineer. + + Enter profile name... + Enter profile name... + No comment provided by engineer. + + + Enter relay name… + Enter relay name… + No comment provided by engineer. + Enter server manually Enter server manually @@ -3259,7 +3634,7 @@ chat item action Error Error - No comment provided by engineer. + conn error description Error aborting address change @@ -3286,6 +3661,11 @@ chat item action Error adding member(s) No comment provided by engineer. + + Error adding relay + Error adding relay + alert title + Error adding server Error adding server @@ -3346,6 +3726,11 @@ chat item action Error creating address No comment provided by engineer. + + Error creating channel + Error creating channel + alert title + Error creating group Error creating group @@ -3481,11 +3866,6 @@ chat item action Error opening chat No comment provided by engineer. - - Error opening group - Error opening group - No comment provided by engineer. - Error receiving file Error receiving file @@ -3531,6 +3911,11 @@ chat item action Error saving ICE servers No comment provided by engineer. + + Error saving channel profile + Error saving channel profile + No comment provided by engineer. + Error saving chat list Error saving chat list @@ -3596,6 +3981,11 @@ chat item action Error setting delivery receipts! No comment provided by engineer. + + Error sharing channel + Error sharing channel + alert title + Error starting chat Error starting chat @@ -3676,7 +4066,8 @@ snd error text Error: %@. Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3920,7 +4311,8 @@ snd error text Fingerprint in server address does not match certificate. Fingerprint in server address does not match certificate. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3962,10 +4354,16 @@ snd error text For all moderators No comment provided by engineer. + + For anyone to reach you + For anyone to reach you + No comment provided by engineer. + For chat profile %@: For chat profile %@: - servers error + servers error +servers warning For console @@ -4106,11 +4504,21 @@ Error: %2$@ GIFs and stickers No comment provided by engineer. + + Get link + Get link + relay test step + Get notified when mentioned. Get notified when mentioned. No comment provided by engineer. + + Get started + Get started + No comment provided by engineer. + Good afternoon! Good afternoon! @@ -4169,7 +4577,7 @@ Error: %2$@ Group link Group link - No comment provided by engineer. + chat link info line Group links @@ -4281,6 +4689,11 @@ Error: %2$@ History is not sent to new members. No comment provided by engineer. + + History is not sent to new subscribers. + History is not sent to new subscribers. + No comment provided by engineer. + How SimpleX works How SimpleX works @@ -4381,11 +4794,6 @@ Error: %2$@ Immediately No comment provided by engineer. - - Immune to spam - Immune to spam - No comment provided by engineer. - Import Import @@ -4528,9 +4936,9 @@ More improvements are coming soon! Initial role No comment provided by engineer. - - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) + + Install SimpleX Chat for terminal + Install SimpleX Chat for terminal No comment provided by engineer. @@ -4588,7 +4996,7 @@ More improvements are coming soon! Invalid connection link Invalid connection link - No comment provided by engineer. + conn error description Invalid display name! @@ -4608,7 +5016,17 @@ More improvements are coming soon! Invalid name! Invalid name! - No comment provided by engineer. + alert title + + + Invalid relay address! + Invalid relay address! + alert title + + + Invalid relay name! + Invalid relay name! + alert title Invalid response @@ -4645,6 +5063,11 @@ More improvements are coming soon! Invite members No comment provided by engineer. + + Invite someone privately + Invite someone privately + No comment provided by engineer. + Invite to chat Invite to chat @@ -4721,6 +5144,11 @@ More improvements are coming soon! Join as %@ No comment provided by engineer. + + Join channel + Join channel + No comment provided by engineer. + Join group Join group @@ -4808,6 +5236,16 @@ This is your link for group %@! Leave swipe action + + Leave channel + Leave channel + No comment provided by engineer. + + + Leave channel? + Leave channel? + No comment provided by engineer. + Leave chat Leave chat @@ -4833,6 +5271,11 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let someone connect to you + Let someone connect to you + No comment provided by engineer. + Let's talk in SimpleX Chat Let's talk in SimpleX Chat @@ -4853,6 +5296,11 @@ This is your link for group %@! Link mobile and desktop apps! 🔗 No comment provided by engineer. + + Link signature verified. + Link signature verified. + owner verification + Linked desktop options Linked desktop options @@ -5038,6 +5486,11 @@ This is your link for group %@! Members can add message reactions. No comment provided by engineer. + + Members can chat with admins. + Members can chat with admins. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) Members can irreversibly delete sent messages. (24 hours) @@ -5103,6 +5556,11 @@ This is your link for group %@! Message draft No comment provided by engineer. + + Message error + Message error + No comment provided by engineer. + Message forwarded Message forwarded @@ -5198,6 +5656,16 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + E2EE info chat item + Messages in this chat will never be deleted. Messages in this chat will never be deleted. @@ -5228,16 +5696,16 @@ This is your link for group %@! Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery. No comment provided by engineer. + + Migrate + Migrate + No comment provided by engineer. + Migrate device Migrate device No comment provided by engineer. - - Migrate from another device - Migrate from another device - No comment provided by engineer. - Migrate here Migrate here @@ -5358,6 +5826,11 @@ This is your link for group %@! Network & servers No comment provided by engineer. + + Network commitments + Network commitments + No comment provided by engineer. + Network connection Network connection @@ -5368,6 +5841,11 @@ This is your link for group %@! Network decentralization No comment provided by engineer. + + Network error + Network error + conn error description + Network issues - message expired after many attempts to send it. Network issues - message expired after many attempts to send it. @@ -5383,6 +5861,13 @@ This is your link for group %@! Network operator No comment provided by engineer. + + Network routers cannot know +who talks to whom + Network routers cannot know +who talks to whom + No comment provided by engineer. + Network settings Network settings @@ -5398,6 +5883,11 @@ This is your link for group %@! New token status text + + New 1-time link + New 1-time link + No comment provided by engineer. + New Passcode New Passcode @@ -5423,6 +5913,11 @@ This is your link for group %@! New chat experience 🎉 No comment provided by engineer. + + New chat relay + New chat relay + No comment provided by engineer. + New contact request New contact request @@ -5493,11 +5988,33 @@ This is your link for group %@! No No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + No account. No phone. No email. No ID. +The most secure encryption. + No comment provided by engineer. + + + No active relays + No active relays + No comment provided by engineer. + No app password No app password Authentication unavailable + + No chat relays + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + No chat relays enabled. + servers warning + No chats No chats @@ -5643,11 +6160,26 @@ This is your link for group %@! No unread chats No comment provided by engineer. - - No user identifiers. - No user identifiers. + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. + + Non-profit governance + Non-profit governance + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + No comment provided by engineer. + + + Not all relays connected + Not all relays connected + alert title + Not compatible! Not compatible! @@ -5705,7 +6237,7 @@ This is your link for group %@! OK OK - No comment provided by engineer. + alert button Off @@ -5724,11 +6256,21 @@ new chat action Old database No comment provided by engineer. + + On your phone, not on servers. + On your phone, not on servers. + No comment provided by engineer. + One-time invitation link One-time invitation link No comment provided by engineer. + + One-time link + One-time link + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5748,6 +6290,11 @@ Requires compatible VPN. Onion hosts will not be used. No comment provided by engineer. + + Only channel owners can change channel preferences. + Only channel owners can change channel preferences. + No comment provided by engineer. + Only chat owners can change preferences. Only chat owners can change preferences. @@ -5851,7 +6398,8 @@ Requires compatible VPN. Open Open - alert action + alert action +alert button Open Settings @@ -5863,6 +6411,11 @@ Requires compatible VPN. Open changes No comment provided by engineer. + + Open channel + Open channel + new chat action + Open chat Open chat @@ -5883,6 +6436,11 @@ Requires compatible VPN. Open conditions No comment provided by engineer. + + Open external link? + Open external link? + alert title + Open full link Open full link @@ -5903,6 +6461,11 @@ Requires compatible VPN. Open migration to another device authentication reason + + Open new channel + Open new channel + new chat action + Open new chat Open new chat @@ -5948,6 +6511,17 @@ Requires compatible VPN. Operator server alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + No comment provided by engineer. + Or import archive file Or import archive file @@ -5968,6 +6542,11 @@ Requires compatible VPN. Or securely share this file link No comment provided by engineer. + + Or show QR in person or via video call. + Or show QR in person or via video call. + No comment provided by engineer. + Or show this code Or show this code @@ -5978,6 +6557,11 @@ Requires compatible VPN. Or to share privately No comment provided by engineer. + + Or use this QR - print or show online. + Or use this QR - print or show online. + No comment provided by engineer. + Organize chats into lists Organize chats into lists @@ -5995,6 +6579,21 @@ Requires compatible VPN. %@ alert message + + Owner + Owner + No comment provided by engineer. + + + Owners + Owners + No comment provided by engineer. + + + Ownership: you can run your own relays. + Ownership: you can run your own relays. + No comment provided by engineer. + PING count PING count @@ -6050,6 +6649,11 @@ Requires compatible VPN. Paste image No comment provided by engineer. + + Paste link / Scan + Paste link / Scan + No comment provided by engineer. + Paste link to connect! Paste link to connect! @@ -6204,6 +6808,16 @@ Error: %@ Preserve the last message draft, with attachments. No comment provided by engineer. + + Preset relay address + Preset relay address + No comment provided by engineer. + + + Preset relay name + Preset relay name + No comment provided by engineer. + Preset server address Preset server address @@ -6239,14 +6853,14 @@ Error: %@ Privacy policy and conditions of use. No comment provided by engineer. - - Privacy redefined - Privacy redefined + + Privacy: for owners and subscribers. + Privacy: for owners and subscribers. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. - Private chats, groups and your contacts are not accessible to server operators. + + Private and secure messaging. + Private and secure messaging. No comment provided by engineer. @@ -6289,6 +6903,11 @@ Error: %@ Private routing timeout alert title + + Proceed + Proceed + alert action + Profile and server connections Profile and server connections @@ -6314,9 +6933,9 @@ Error: %@ Profile theme No comment provided by engineer. - - Profile update will be sent to your contacts. - Profile update will be sent to your contacts. + + Profile update will be sent to your SimpleX contacts. + Profile update will be sent to your SimpleX contacts. alert message @@ -6324,6 +6943,11 @@ Error: %@ Prohibit audio/video calls. No comment provided by engineer. + + Prohibit chats with admins. + Prohibit chats with admins. + No comment provided by engineer. + Prohibit irreversible message deletion. Prohibit irreversible message deletion. @@ -6354,6 +6978,11 @@ Error: %@ Prohibit sending direct messages to members. No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + Prohibit sending direct messages to subscribers. + No comment provided by engineer. + Prohibit sending disappearing messages. Prohibit sending disappearing messages. @@ -6421,6 +7050,11 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. + + Public channels - speak freely 🚀 + Public channels - speak freely 🚀 + No comment provided by engineer. + Push notifications Push notifications @@ -6461,24 +7095,14 @@ Enable in *Network & servers* settings. Read more No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + Read more in User Guide. No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - No comment provided by engineer. - - - Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - Read more in [User Guide](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). - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). + + Read more in our GitHub repository. + Read more in our GitHub repository. No comment provided by engineer. @@ -6638,6 +7262,31 @@ swipe action Reject member? alert title + + Relay + Relay + No comment provided by engineer. + + + Relay address + Relay address + alert title + + + Relay connection failed + Relay connection failed + alert title + + + Relay link + Relay link + No comment provided by engineer. + + + Relay results: + Relay results: + alert message + Relay server is only used if necessary. Another party can observe your IP address. Relay server is only used if necessary. Another party can observe your IP address. @@ -6648,6 +7297,16 @@ swipe action Relay server protects your IP address, but it can observe the duration of the call. No comment provided by engineer. + + Relay test failed! + Relay test failed! + No comment provided by engineer. + + + Reliability: many relays per channel. + Reliability: many relays per channel. + No comment provided by engineer. + Remove Remove @@ -6688,6 +7347,16 @@ swipe action Remove passphrase from keychain? No comment provided by engineer. + + Remove subscriber + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + Remove subscriber? + alert title + Removes messages and blocks members. Removes messages and blocks members. @@ -6923,6 +7592,11 @@ swipe action SOCKS proxy No comment provided by engineer. + + Safe web links + Safe web links + No comment provided by engineer. + Safely receive files Safely receive files @@ -6949,6 +7623,11 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + Save (and notify subscribers) + alert button + Save admission settings? Save admission settings? @@ -6964,6 +7643,11 @@ chat item action Save and notify group members No comment provided by engineer. + + Save and notify subscribers + Save and notify subscribers + No comment provided by engineer. + Save and reconnect Save and reconnect @@ -6974,6 +7658,16 @@ chat item action Save and update group profile No comment provided by engineer. + + Save channel profile + Save channel profile + No comment provided by engineer. + + + Save channel profile? + Save channel profile? + alert title + Save group profile Save group profile @@ -7154,6 +7848,11 @@ chat item action Security code No comment provided by engineer. + + Security: owners hold channel keys. + Security: owners hold channel keys. + No comment provided by engineer. + Select Select @@ -7284,6 +7983,11 @@ chat item action Send request without message No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. Send them from gallery or custom keyboards. @@ -7294,6 +7998,11 @@ chat item action Send up to 100 last messages to new members. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + Send up to 100 last messages to new subscribers. + No comment provided by engineer. + Send your private feedback to groups. Send your private feedback to groups. @@ -7309,6 +8018,11 @@ chat item action Sender may have deleted the connection request. No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. Sending delivery receipts will be enabled for all contacts in all visible chat profiles. @@ -7434,6 +8148,11 @@ chat item action Server protocol changed. alert title + + Server requires authorization to connect to relay, check password. + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Server requires authorization to create queues, check password. @@ -7564,6 +8283,16 @@ chat item action Settings were changed. alert message + + Setup notifications + Setup notifications + No comment provided by engineer. + + + Setup routers + Setup routers + No comment provided by engineer. + Shape profile images Shape profile images @@ -7600,11 +8329,16 @@ chat item action Share address publicly No comment provided by engineer. - - Share address with contacts? - Share address with contacts? + + Share address with SimpleX contacts? + Share address with SimpleX contacts? alert title + + Share channel + Share channel + No comment provided by engineer. + Share from other apps. Share from other apps. @@ -7630,6 +8364,11 @@ chat item action Share profile No comment provided by engineer. + + Share relay address + Share relay address + No comment provided by engineer. + Share this 1-time invite link Share this 1-time invite link @@ -7640,9 +8379,14 @@ chat item action Share to SimpleX No comment provided by engineer. - - Share with contacts - Share with contacts + + Share via chat + Share via chat + No comment provided by engineer. + + + Share with SimpleX contacts + Share with SimpleX contacts No comment provided by engineer. @@ -7815,9 +8559,9 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. - - SimpleX relay link - SimpleX relay link + + SimpleX relay address + SimpleX relay address simplex link type @@ -7893,6 +8637,11 @@ report reason Square, circle, or anything in between. No comment provided by engineer. + + Star on GitHub + Star on GitHub + No comment provided by engineer. + Start chat Start chat @@ -7993,6 +8742,78 @@ report reason Subscribed No comment provided by engineer. + + Subscriber + Subscriber + No comment provided by engineer. + + + Subscriber reports + Subscriber reports + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + Subscribers + No comment provided by engineer. + + + Subscribers can add message reactions. + Subscribers can add message reactions. + No comment provided by engineer. + + + Subscribers can chat with admins. + Subscribers can chat with admins. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + Subscribers can irreversibly delete sent messages. (24 hours) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + Subscribers can report messsages to moderators. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + Subscribers can send SimpleX links. + No comment provided by engineer. + + + Subscribers can send direct messages. + Subscribers can send direct messages. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + Subscribers can send disappearing messages. + No comment provided by engineer. + + + Subscribers can send files and media. + Subscribers can send files and media. + No comment provided by engineer. + + + Subscribers can send voice messages. + Subscribers can send voice messages. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Subscription errors @@ -8073,6 +8894,11 @@ report reason Take picture No comment provided by engineer. + + Talk to someone + Talk to someone + No comment provided by engineer. + Tap Connect to chat Tap Connect to chat @@ -8088,9 +8914,9 @@ report reason Tap Connect to use bot No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. - Tap Create SimpleX address in the menu to create it later. + + Tap Join channel + Tap Join channel No comment provided by engineer. @@ -8123,6 +8949,11 @@ report reason Tap to join incognito No comment provided by engineer. + + Tap to open + Tap to open + No comment provided by engineer. + Tap to paste link Tap to paste link @@ -8141,13 +8972,19 @@ report reason Test failed at step %@. Test failed at step %@. - server test failure + relay test failure +server test failure Test notifications Test notifications No comment provided by engineer. + + Test relay + Test relay + No comment provided by engineer. + Test server Test server @@ -8200,6 +9037,11 @@ It can happen because of some bug or when the connection is compromised.The app protects your privacy by using different operators in each conversation. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). The app will ask to confirm downloads from unknown file servers (except .onion). @@ -8215,6 +9057,11 @@ It can happen because of some bug or when the connection is compromised.The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages + The connection reached the limit of undelivered messages + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. The connection reached the limit of undelivered messages, your contact may be offline. @@ -8240,9 +9087,11 @@ It can happen because of some bug or when the connection is compromised.The encryption is working and the new encryption agreement is not required. It may result in connection errors! No comment provided by engineer. - - The future of messaging - The future of messaging + + The first network where you own +your contacts and groups. + The first network where you own +your contacts and groups. No comment provided by engineer. @@ -8280,6 +9129,11 @@ It can happen because of some bug or when the connection is compromised.The old database was not removed during the migration, it can be deleted. No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + No comment provided by engineer. + The same conditions will apply to operator **%@**. The same conditions will apply to operator **%@**. @@ -8325,6 +9179,16 @@ It can happen because of some bug or when the connection is compromised.Themes No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + No comment provided by engineer. + These conditions will also apply for: **%@**. These conditions will also apply for: **%@**. @@ -8390,6 +9254,16 @@ It can happen because of some bug or when the connection is compromised.This group no longer exists. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. @@ -8440,6 +9314,11 @@ It can happen because of some bug or when the connection is compromised.To hide unwanted messages. No comment provided by engineer. + + To make SimpleX Network last. + To make SimpleX Network last. + No comment provided by engineer. + To make a new connection To make a new connection @@ -8527,11 +9406,6 @@ You will be prompted to complete authentication before this feature is enabled.< To verify end-to-end encryption with your contact compare (or scan) the code on your devices. No comment provided by engineer. - - Toggle chat list: - Toggle chat list: - No comment provided by engineer. - Toggle incognito when connecting. Toggle incognito when connecting. @@ -8547,6 +9421,11 @@ You will be prompted to complete authentication before this feature is enabled.< Toolbar opacity No comment provided by engineer. + + Top bar + Top bar + No comment provided by engineer. + Total Total @@ -8612,6 +9491,11 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. + + Unblock subscriber for all? + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Undelivered messages @@ -8712,13 +9596,18 @@ To connect, please ask your contact to create another connection link and check Unsupported connection link Unsupported connection link - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. Up to 100 last messages are sent to new members. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + Up to 100 last messages are sent to new subscribers. + No comment provided by engineer. + Update Update @@ -8844,11 +9733,6 @@ To connect, please ask your contact to create another connection link and check Use TCP port 443 for preset servers only. No comment provided by engineer. - - Use chat - Use chat - No comment provided by engineer. - Use current profile Use current profile @@ -8864,6 +9748,11 @@ To connect, please ask your contact to create another connection link and check Use for messages No comment provided by engineer. + + Use for new channels + Use for new channels + No comment provided by engineer. + Use for new connections Use for new connections @@ -8904,6 +9793,11 @@ To connect, please ask your contact to create another connection link and check Use private routing with unknown servers. No comment provided by engineer. + + Use relay + Use relay + No comment provided by engineer. + Use server Use server @@ -8924,6 +9818,11 @@ To connect, please ask your contact to create another connection link and check Use the app with one hand. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + Use this address in your social media profile, website, or email signature. + No comment provided by engineer. + Use web port Use web port @@ -8944,6 +9843,11 @@ To connect, please ask your contact to create another connection link and check Using SimpleX Chat servers. No comment provided by engineer. + + Verify + Verify + relay test step + Verify code with desktop Verify code with desktop @@ -9064,6 +9968,21 @@ To connect, please ask your contact to create another connection link and check Voice message… No comment provided by engineer. + + Wait + Wait + alert action + + + Wait response + Wait response + relay test step + + + Waiting for channel owner to add relays. + Waiting for channel owner to add relays. + No comment provided by engineer. + Waiting for desktop... Waiting for desktop... @@ -9104,6 +10023,11 @@ To connect, please ask your contact to create another connection link and check Warning: you may lose some data! No comment provided by engineer. + + We made connecting simpler for new users. + We made connecting simpler for new users. + No comment provided by engineer. + WebRTC ICE servers WebRTC ICE servers @@ -9154,6 +10078,11 @@ To connect, please ask your contact to create another connection link and check 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. + + Why SimpleX is built. + Why SimpleX is built. + No comment provided by engineer. + WiFi WiFi @@ -9366,6 +10295,11 @@ Repeat join request? 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 channel. + You can share a link or a QR code - anybody will be able to join the channel. + 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. 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. @@ -9411,16 +10345,25 @@ Repeat join request? You can't send messages! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + You commit to: +- Only legal content in public groups +- Respect other users - no spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. You could not be verified; please try again. No comment provided by engineer. - - You decide who can connect. - You decide who can connect. - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -9488,6 +10431,11 @@ Repeat connection request? You should receive notifications. token info + + You were born without an account + You were born without an account + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. You will be able to send messages **only after your request is accepted**. @@ -9523,6 +10471,11 @@ Repeat connection request? 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 channel. Chat history will be preserved. + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. You will stop receiving messages from this chat. Chat history will be preserved. @@ -9568,6 +10521,11 @@ Repeat connection request? Your calls No comment provided by engineer. + + Your channel + Your channel + No comment provided by engineer. + Your chat database Your chat database @@ -9618,6 +10576,11 @@ Repeat connection request? Your contacts will remain connected. No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + No comment provided by engineer. + Your credentials may be sent unencrypted. Your credentials may be sent unencrypted. @@ -9638,6 +10601,11 @@ Repeat connection request? Your group No comment provided by engineer. + + Your network + Your network + No comment provided by engineer. + Your preferences Your preferences @@ -9653,6 +10621,13 @@ Repeat connection request? Your profile No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Your profile **%@** will be shared. @@ -9673,11 +10648,26 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. alert message + + Your public address + Your public address + No comment provided by engineer. + Your random profile Your random profile No comment provided by engineer. + + Your relay address + Your relay address + No comment provided by engineer. + + + Your relay name + Your relay name + No comment provided by engineer. + Your server address Your server address @@ -9693,21 +10683,11 @@ Repeat connection request? Your settings No comment provided by engineer. - - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - No comment provided by engineer. - [Send us email](mailto:chat@simplex.chat) [Send us email](mailto:chat@simplex.chat) No comment provided by engineer. - - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - \_italic_ \_italic_ @@ -9723,6 +10703,11 @@ Repeat connection request? above, then choose: No comment provided by engineer. + + accepted + accepted + No comment provided by engineer. + accepted %@ accepted %@ @@ -9743,6 +10728,11 @@ Repeat connection request? accepted you rcv group event chat item + + active + active + No comment provided by engineer. + admin admin @@ -9854,6 +10844,11 @@ marked deleted chat item preview text calling… call status + + can't broadcast + can't broadcast + No comment provided by engineer. + can't send messages can't send messages @@ -9889,6 +10884,16 @@ marked deleted chat item preview text changing address… chat item text + + channel + channel + shown as sender role for channel messages + + + channel profile updated + channel profile updated + snd group event chat item + colored colored @@ -10035,6 +11040,11 @@ pref value deleted deleted chat item + + deleted channel + deleted channel + rcv group event chat item + deleted contact deleted contact @@ -10145,6 +11155,11 @@ pref value error No comment provided by engineer. + + error: %@ + error: %@ + receive error chat item + expired expired @@ -10275,6 +11290,11 @@ pref value left rcv group event chat item + + link + link + No comment provided by engineer. + marked deleted marked deleted @@ -10345,6 +11365,11 @@ pref value never delete after time + + new + new + No comment provided by engineer. + new message new message @@ -10468,6 +11493,11 @@ time to disappear rejected call call status + + relay + relay + member role + removed removed @@ -10478,6 +11508,16 @@ time to disappear removed %@ rcv group event chat item + + removed (%d attempts) + removed (%d attempts) + receive error chat item + + + removed by operator + removed by operator + No comment provided by engineer. + removed contact address removed contact address @@ -10632,6 +11672,11 @@ last received msg: %2$@ unprotected No comment provided by engineer. + + updated channel profile + updated channel profile + rcv group event chat item + updated group profile updated group profile @@ -10652,6 +11697,11 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + via %@ + relay hostname + via contact address link via contact address link @@ -10727,6 +11777,11 @@ last received msg: %2$@ you are observer No comment provided by engineer. + + you are subscriber + you are subscriber + No comment provided by engineer. + you blocked %@ you blocked %@ @@ -10787,6 +11842,11 @@ last received msg: %2$@ \~strike~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + ⚠️ Signature verification failed: %@. + owner verification + 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 61734f2480..43d3895cc4 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -185,6 +185,24 @@ %d mes(es) time interval + + %d relays failed + %d servidores han fallado + channel relay bar +channel subscriber relay bar + + + %d relays not active + %d servidores inactivos + channel relay bar +channel subscriber relay bar + + + %d relays removed + %d servidores eliminados + channel relay bar +channel subscriber relay bar + %d sec %d segundo(s) @@ -200,11 +218,63 @@ %d mensaje(s) omitido(s) integrity error chat item + + %d subscriber + %d suscriptor + channel subscriber count + + + %d subscribers + %d suscriptores + channel subscriber count + %d weeks %d semana(s) time interval + + %1$d/%2$d relays active + %1$d/%2$d servidores activos + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + %1$d/%2$d servidores activos, %3$d errores + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + %1$d/%2$d servidores activos, %3$d han fallado + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + %1$d/%2$d servidores activos, %3$d servidores eliminados + channel relay bar + + + %1$d/%2$d relays connected + %1$d/%2$d servidores conectados + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + %1$d/%2$d servidores conectados, %3$d errores + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + %1$d/%2$d servidores conectados, %3$d con fallo + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + %1$d/%2$d servidores conectados, %3$d eliminados + channel subscriber relay bar + %lld %lld @@ -215,6 +285,11 @@ %lld %@ No comment provided by engineer. + + %lld channel events + %lld eventos del canal + No comment provided by engineer. + %lld contact(s) selected %lld contacto(s) seleccionado(s) @@ -227,7 +302,7 @@ %lld group events - %lld evento(s) de grupo + %lld evento(s) del grupo No comment provided by engineer. @@ -315,11 +390,21 @@ %u mensaje(s) omitido(s). No comment provided by engineer. + + (from owner) + (del propietario) + chat link info line + (new) (nuevo) No comment provided by engineer. + + (signed) + (firmado) + chat link info line + (this device v%@) (este dispositivo v%@) @@ -365,6 +450,11 @@ **Escanear / Pegar enlace**: para conectar mediante un enlace recibido. No comment provided by engineer. + + **Test relay** to retrieve its name. + **Test servidor** para recibir su nombre. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Advertencia**: Las notificaciones automáticas instantáneas requieren una contraseña guardada en Keychain. @@ -408,6 +498,15 @@ - ¡y más! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + - aceptar el envío de vistas previas de los enlaces. +- prevenir el phishing mediante hipervínculos. +- eliminar el seguimiento de los enlaces. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -506,6 +605,11 @@ time interval Algunas cosas más No comment provided by engineer. + + A link for one person to connect + Enlace para un solo contacto + No comment provided by engineer. + A new contact Contacto nuevo @@ -632,9 +736,9 @@ swipe action Conexiones activas 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. - 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. + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. + Añade la dirección a tu perfil para que tus contactos SimpleX puedan compartirla con otros. La actualización del perfil se enviará a tus contactos SimpleX. No comment provided by engineer. @@ -702,6 +806,11 @@ swipe action Servidores de mensajes añadidos No comment provided by engineer. + + Adding relays will be supported later. + Añadir servidores estará disponible en una versión posterior. + No comment provided by engineer. + Additional accent Acento adicional @@ -822,6 +931,16 @@ swipe action Todos los perfiles profile dropdown + + All relays failed + Todos los servidores han fallado + No comment provided by engineer. + + + All relays removed + Todos los servidores eliminados + No comment provided by engineer. + All reports will be archived for you. Todos los informes serán archivados para ti. @@ -882,6 +1001,11 @@ swipe action Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite. (24 horas) No comment provided by engineer. + + Allow members to chat with admins. + Permitir que los miembros chateen con administradores. + No comment provided by engineer. + Allow message reactions only if your contact allows them. Se permiten las reacciones a los mensajes pero sólo si tu contacto también las permite. @@ -897,6 +1021,11 @@ swipe action Se permiten mensajes directos entre miembros. No comment provided by engineer. + + Allow sending direct messages to subscribers. + Se permiten mensajes directos entre suscriptores. + No comment provided by engineer. + Allow sending disappearing messages. Permites el envío de mensajes temporales. @@ -907,6 +1036,11 @@ swipe action Permitir compartir No comment provided by engineer. + + Allow subscribers to chat with admins. + Permitir que los suscriptores chateen con administradores. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Se permite la eliminación irreversible de mensajes. (24 horas) @@ -1012,11 +1146,6 @@ swipe action Responder llamada No comment provided by engineer. - - Anybody can host servers. - Cualquiera puede alojar servidores. - No comment provided by engineer. - App build: %@ Compilación app: %@ @@ -1222,6 +1351,23 @@ swipe action Hash de mensaje incorrecto No comment provided by engineer. + + Be free +in your network + Se libre +en tu red + No comment provided by engineer. + + + Be free in your network. + Se libre en tu red. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + Porque hemos destruido el poder de saber quien eres. De manera que tu poder nunca se pueda arrebatar. + No comment provided by engineer. + Better calls Llamadas mejoradas @@ -1317,6 +1463,11 @@ swipe action ¿Bloquear miembro? No comment provided by engineer. + + Block subscriber for all? + ¿Bloquear al suscriptor para todos? + No comment provided by engineer. + Blocked by admin Bloqueado por administrador @@ -1367,6 +1518,16 @@ swipe action Tanto tú como tu contacto podéis enviar mensajes de voz. No comment provided by engineer. + + Bottom bar + Barra inferior + No comment provided by engineer. + + + Broadcast + Emisión + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Búlgaro, Finlandés, Tailandés y Ucraniano - gracias a los usuarios y [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1375,7 +1536,7 @@ swipe action Business address Dirección empresarial - No comment provided by engineer. + chat link info line Business chats @@ -1397,15 +1558,6 @@ swipe action Mediante perfil (predeterminado) 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. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - Al usar SimpleX Chat, aceptas: -- enviar únicamente contenido legal en los grupos públicos. -- respetar a los demás usuarios – spam prohibido. - No comment provided by engineer. - Call already ended! ¡La llamada ha terminado! @@ -1554,6 +1706,82 @@ new chat action authentication reason set passcode view + + Channel + Canal + No comment provided by engineer. + + + Channel display name + Título mostrado del canal + No comment provided by engineer. + + + Channel full name (optional) + Título completo del canal (opcional) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + El canal no tiene servidores activos. Por favor, intenta unirte más tarde. + alert message +alert subtitle + + + Channel image + Imagen del canal + No comment provided by engineer. + + + Channel link + Enlace del canal + chat link info line + + + Channel preferences + Preferencias del canal + No comment provided by engineer. + + + Channel profile + Perfil del canal + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + El perfil del canal se almacena en los dispositivos de los suscriptores y en los servidores de chat. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + El perfil del canal ha sido modificado. Si lo guardas, el perfil actualizado será enviado a los suscriptores. + alert message + + + Channel temporarily unavailable + Canales no disponibles temporalmente + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + El canal será eliminado para todos los suscriptores. ¡No puede deshacerse! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + El canal será eliminado para tí. ¡No puede deshacerse! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + El canal comenzará a funcionar con %1$d de %2$d servidores. ¿Continuar? + alert message + + + Channels + Canales + No comment provided by engineer. + Chat Chat @@ -1639,6 +1867,26 @@ set passcode view Perfil de usuario No comment provided by engineer. + + Chat relay + Servidor de chat + No comment provided by engineer. + + + Chat relays + Servidores de chat + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + Los servidores de chat reenvían los mensajes en los canales que has creado. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + Los servidores de chat reenvían los mensajes a los suscriptores del canal. + No comment provided by engineer. + Chat theme Tema de chat @@ -1657,7 +1905,8 @@ set passcode view Chat with admins Chatea con administradores - chat toolbar + chat feature +chat toolbar Chat with member @@ -1674,11 +1923,26 @@ set passcode view Chats No comment provided by engineer. + + Chats with admins are prohibited. + Chat con administradores no permitido. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + El chat con administradores en el canal público no dispone de cifrado E2E. Úsalo sólo con servidores de confianza. + alert message + Chats with members Chat con miembros No comment provided by engineer. + + Chats with members are disabled + Chats con miembros desactivado + No comment provided by engineer. + Check messages every 20 min. Comprobar mensajes cada 20 min. @@ -1689,6 +1953,16 @@ set passcode view Comprobar mensajes cuando se permita. No comment provided by engineer. + + Check relay address and try again. + Comprueba la dirección del servidor y prueba de nuevo. + alert message + + + Check relay name and try again. + Comprueba el nombre del servidor y prueba de nuevo. + alert message + Check server address and try again. Comprueba la dirección del servidor e inténtalo de nuevo. @@ -1834,9 +2108,9 @@ set passcode view Configure servidores ICE No comment provided by engineer. - - Configure server operators - Configurar operadores de servidores + + Configure relays + Configurar servidores No comment provided by engineer. @@ -1897,7 +2171,8 @@ set passcode view Connect Conectar - server test step + relay test step +server test step Connect automatically @@ -1943,6 +2218,11 @@ This is your own one-time link! Conectar mediante enlace new chat sheet title + + Connect via link or QR code + Conecta vía enlace o QR + No comment provided by engineer. + Connect via one-time link Conectar mediante enlace de un sólo uso @@ -2021,10 +2301,11 @@ This is your own one-time link! Connection error (AUTH) Error de conexión (Autenticación) - No comment provided by engineer. + conn error description Connection failed + Conexión fallida No comment provided by engineer. @@ -2079,6 +2360,11 @@ This is your own one-time link! Conexiones No comment provided by engineer. + + Contact address + Dirección de contacto + chat link info line + Contact allows El contacto permite @@ -2149,6 +2435,11 @@ This is your own one-time link! Continuar No comment provided by engineer. + + Contribute + Contribuye + No comment provided by engineer. + Conversation deleted! ¡Conversación eliminada! @@ -2177,12 +2468,7 @@ This is your own one-time link! Correct name to %@? ¿Corregir el nombre a %@? - No comment provided by engineer. - - - Create - Crear - No comment provided by engineer. + alert message Create 1-time link @@ -2234,6 +2520,16 @@ This is your own one-time link! Crear perfil No comment provided by engineer. + + Create public channel + Crear canal público + No comment provided by engineer. + + + Create public channel (BETA) + Crear canal público (BETA) + No comment provided by engineer. + Create queue Crear cola @@ -2244,11 +2540,21 @@ This is your own one-time link! Crea tu dirección No comment provided by engineer. + + Create your link + Crea tu enlace + No comment provided by engineer. + Create your profile Crea tu perfil No comment provided by engineer. + + Create your public address + Crea tu dirección pública + No comment provided by engineer. + Created Creadas @@ -2269,6 +2575,11 @@ This is your own one-time link! Creando enlace al archivo No comment provided by engineer. + + Creating channel + Creando canal + No comment provided by engineer. + Creating link… Creando enlace… @@ -2427,10 +2738,10 @@ This is your own one-time link! Informe debug No comment provided by engineer. - - Decentralized - Descentralizada - No comment provided by engineer. + + Decode link + Decodificar enlace + relay test step Decryption error @@ -2478,6 +2789,16 @@ swipe action Eliminar y notificar contacto No comment provided by engineer. + + Delete channel + Eliminar canal + No comment provided by engineer. + + + Delete channel? + ¿Eliminar el canal? + No comment provided by engineer. + Delete chat Eliminar chat @@ -2649,6 +2970,11 @@ alert button Eliminar cola server test step + + Delete relay + Eliminar servidor + No comment provided by engineer. + Delete report Eliminar informe @@ -2814,6 +3140,16 @@ alert button Los mensajes directos entre miembros del grupo no están permitidos. No comment provided by engineer. + + Direct messages between subscribers are prohibited. + Los mensajes directos entre suscriptores del canal no están permitidos. + No comment provided by engineer. + + + Disable + Desactivar + alert button + Disable (keep overrides) Desactivar (conservando anulaciones) @@ -2831,7 +3167,7 @@ alert button Disable delete messages - Desactivar + Desactivar eliminar mensajes alert button @@ -2919,6 +3255,11 @@ alert button No se envía el historial a los miembros nuevos. No comment provided by engineer. + + Do not send history to new subscribers. + No se envía el historial a los suscriptores nuevos. + No comment provided by engineer. + Do not use credentials with proxy. No se usan credenciales con proxy. @@ -3020,11 +3361,21 @@ chat item action Notificaciones cifradas E2E. No comment provided by engineer. + + Easier to invite your friends 👋 + Invitar a tus amigos es más fácil 👋 + No comment provided by engineer. + Edit Editar chat item action + + Edit channel profile + Editar perfil del canal + No comment provided by engineer. + Edit group profile Editar perfil de grupo @@ -3038,7 +3389,7 @@ chat item action Enable Activar - No comment provided by engineer. + alert button Enable (keep overrides) @@ -3060,6 +3411,11 @@ chat item action Activar TCP keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + Activar al menos un servidor de chat en Servidores y Redes. + channel creation warning + Enable automatic message deletion? ¿Activar eliminación automática de mensajes? @@ -3070,6 +3426,11 @@ chat item action Permitir acceso a la cámara No comment provided by engineer. + + Enable chats with admins? + ¿Activar chat con administradores? + alert title + Enable disappearing messages by default. Activa por defecto los mensajes temporales. @@ -3090,16 +3451,16 @@ chat item action ¿Activar notificaciones instantáneas? No comment provided by engineer. + + Enable link previews? + ¿Activar previsualización de enlaces? + alert title + Enable lock Activar bloqueo No comment provided by engineer. - - Enable notifications - Activar notificaciones - No comment provided by engineer. - Enable periodic notifications? ¿Activar notificaciones periódicas? @@ -3205,6 +3566,11 @@ chat item action Introduce Código No comment provided by engineer. + + Enter channel name… + Introduce el título del canal… + No comment provided by engineer. + Enter correct passphrase. Introduce la contraseña correcta. @@ -3230,6 +3596,16 @@ chat item action ¡Introduce la contraseña arriba para mostrar! No comment provided by engineer. + + Enter profile name... + Introduce el nombre del perfil… + No comment provided by engineer. + + + Enter relay name… + Introduce el nombre del servidor… + No comment provided by engineer. + Enter server manually Añadir manualmente @@ -3258,7 +3634,7 @@ chat item action Error Error - No comment provided by engineer. + conn error description Error aborting address change @@ -3285,6 +3661,11 @@ chat item action Error al añadir miembro(s) No comment provided by engineer. + + Error adding relay + Error al añadir el servidor + alert title + Error adding server Error al añadir servidor @@ -3345,6 +3726,11 @@ chat item action Error al crear dirección No comment provided by engineer. + + Error creating channel + Error al crear el canal + alert title + Error creating group Error al crear grupo @@ -3480,11 +3866,6 @@ chat item action Error al abrir chat No comment provided by engineer. - - Error opening group - Error al abrir el grupo - No comment provided by engineer. - Error receiving file Error al recibir archivo @@ -3530,6 +3911,11 @@ chat item action Error al guardar servidores ICE No comment provided by engineer. + + Error saving channel profile + Error al guardar el perfil del canal + No comment provided by engineer. + Error saving chat list Error al guardar listas @@ -3595,6 +3981,11 @@ chat item action ¡Error al configurar confirmaciones de entrega! No comment provided by engineer. + + Error sharing channel + Error al compartir el canal + alert title + Error starting chat Error al iniciar Chat @@ -3675,7 +4066,8 @@ snd error text Error: %@. Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3919,7 +4311,8 @@ snd error text Fingerprint in server address does not match certificate. La huella en la dirección del servidor no coincide con el certificado. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3961,10 +4354,16 @@ snd error text Para todos los moderadores No comment provided by engineer. + + For anyone to reach you + Cualquiera puede contactarte + No comment provided by engineer. + For chat profile %@: Para el perfil de chat %@: - servers error + servers error +servers warning For console @@ -4105,11 +4504,21 @@ Error: %2$@ GIFs y stickers No comment provided by engineer. + + Get link + Recibir el enlace + relay test step + Get notified when mentioned. Las menciones ahora se notifican. No comment provided by engineer. + + Get started + Empezar + No comment provided by engineer. + Good afternoon! ¡Buenas tardes! @@ -4168,7 +4577,7 @@ Error: %2$@ Group link Enlace de grupo - No comment provided by engineer. + chat link info line Group links @@ -4280,6 +4689,11 @@ Error: %2$@ El historial no se envía a miembros nuevos. No comment provided by engineer. + + History is not sent to new subscribers. + El historial no se envía a suscriptores nuevos. + No comment provided by engineer. + How SimpleX works Cómo funciona SimpleX @@ -4347,6 +4761,7 @@ Error: %2$@ If you joined or created channels, they will stop working permanently. + Si te has unido o has creado canales, dejarán de funcionar permanentemente. down migration warning @@ -4379,11 +4794,6 @@ Error: %2$@ Inmediatamente No comment provided by engineer. - - Immune to spam - Inmune a spam y abuso - No comment provided by engineer. - Import Importar @@ -4526,9 +4936,9 @@ More improvements are coming soon! Rol inicial No comment provided by engineer. - - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - Instalar terminal para [SimpleX Chat](https://github.com/simplex-chat/simplex-chat) + + Install SimpleX Chat for terminal + Instalar terminal para SimpleX Chat No comment provided by engineer. @@ -4586,7 +4996,7 @@ More improvements are coming soon! Invalid connection link Enlace de conexión no válido - No comment provided by engineer. + conn error description Invalid display name! @@ -4606,7 +5016,17 @@ More improvements are coming soon! Invalid name! ¡Nombre no válido! - No comment provided by engineer. + alert title + + + Invalid relay address! + ¡Dirección de servidor no válido! + alert title + + + Invalid relay name! + ¡Nombre de servidor no válido! + alert title Invalid response @@ -4643,6 +5063,11 @@ More improvements are coming soon! Invitar miembros No comment provided by engineer. + + Invite someone privately + Invitación privada + No comment provided by engineer. + Invite to chat Invitar al chat @@ -4719,6 +5144,11 @@ More improvements are coming soon! Unirme como %@ No comment provided by engineer. + + Join channel + Unirme al canal + No comment provided by engineer. + Join group Unirme al grupo @@ -4806,6 +5236,16 @@ This is your link for group %@! Salir swipe action + + Leave channel + Salir del canal + No comment provided by engineer. + + + Leave channel? + ¿Salir del canal? + No comment provided by engineer. + Leave chat Salir del chat @@ -4831,6 +5271,11 @@ This is your link for group %@! Menos tráfico en redes móviles. No comment provided by engineer. + + Let someone connect to you + Conecta con alguien + No comment provided by engineer. + Let's talk in SimpleX Chat Hablemos en SimpleX Chat @@ -4851,6 +5296,11 @@ This is your link for group %@! ¡Enlazar aplicación móvil con ordenador! 🔗 No comment provided by engineer. + + Link signature verified. + Firma del enlace verificada. + owner verification + Linked desktop options Opciones ordenador enlazado @@ -5036,6 +5486,11 @@ This is your link for group %@! Los miembros pueden añadir reacciones a los mensajes. No comment provided by engineer. + + Members can chat with admins. + Los miembros pueden chatear con los administradores. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas) @@ -5101,6 +5556,11 @@ This is your link for group %@! Borrador de mensaje No comment provided by engineer. + + Message error + Mensaje de error + No comment provided by engineer. + Message forwarded Mensaje reenviado @@ -5196,6 +5656,16 @@ This is your link for group %@! ¡Los mensajes nuevos de %@ serán mostrados! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + Los mensajes en este canal **no están cifrados de extremo a extremo**. Los servidores pueden ver estos mensajes. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + Los mensajes en este canal no están cifrados de extremo a extremo. Los servidores pueden ver estos mensajes. + E2EE info chat item + Messages in this chat will never be deleted. Los mensajes de esta conversación nunca se eliminan. @@ -5226,16 +5696,16 @@ This is your link for group %@! Los mensajes, archivos y llamadas están protegidos mediante **cifrado de extremo a extremo resistente a tecnología cuántica** con secreto perfecto hacía adelante, repudio y recuperación tras ataque. No comment provided by engineer. + + Migrate + Migrar + No comment provided by engineer. + Migrate device Migrar dispositivo No comment provided by engineer. - - Migrate from another device - Migrar desde otro dispositivo - No comment provided by engineer. - Migrate here Migrar aquí @@ -5356,6 +5826,11 @@ This is your link for group %@! Servidores y Redes No comment provided by engineer. + + Network commitments + Compromisos en la red + No comment provided by engineer. + Network connection Conexión de red @@ -5366,6 +5841,11 @@ This is your link for group %@! Descentralización de la red No comment provided by engineer. + + Network error + Error de red + conn error description + Network issues - message expired after many attempts to send it. Problema en la red - el mensaje ha expirado tras muchos intentos de envío. @@ -5381,6 +5861,13 @@ This is your link for group %@! Operador de red No comment provided by engineer. + + Network routers cannot know +who talks to whom + Los routers de la red no pueden saber +quién se comunica con quién + No comment provided by engineer. + Network settings Configuración de red @@ -5396,6 +5883,11 @@ This is your link for group %@! Nuevo token status text + + New 1-time link + Nuevo enlace de 1 solo uso + No comment provided by engineer. + New Passcode Código Nuevo @@ -5421,6 +5913,11 @@ This is your link for group %@! Nueva experiencia de chat 🎉 No comment provided by engineer. + + New chat relay + Nuevo servidor de chat + No comment provided by engineer. + New contact request Nueva solicitud de contacto @@ -5491,11 +5988,33 @@ This is your link for group %@! No No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + Sin cuenta. Sin teléfono. Sin email. Sin ID. +El cifrado más seguro. + No comment provided by engineer. + + + No active relays + Sin servidores activos + No comment provided by engineer. + No app password Sin contraseña de la aplicación Authentication unavailable + + No chat relays + Sin servidores de chat + No comment provided by engineer. + + + No chat relays enabled. + Ningún servidor de chat activado. + servers warning + No chats Sin chats @@ -5641,11 +6160,26 @@ This is your link for group %@! Ningún chat sin leer No comment provided by engineer. - - No user identifiers. - Sin identificadores de usuario. + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. + Nadie monitorizaba tus conversaciones. Nadie registraba tus ubicaciones. La privacidad nunca fue un lujo, era la manera de vivir. No comment provided by engineer. + + Non-profit governance + Gobernanza no lucrativa + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + No un candado mejorado en la puerta de otro. No un terrateniente que respeta tu privacidad pero sigue guardando un registro de tus visitantes. Tu no eres el invitado. Estás en tu casa y ningún rey podrá entrar. Tu eres el soberano. + No comment provided by engineer. + + + Not all relays connected + Hay servidores no conectados + alert title + Not compatible! ¡No compatible! @@ -5703,7 +6237,7 @@ This is your link for group %@! OK OK - No comment provided by engineer. + alert button Off @@ -5722,11 +6256,21 @@ new chat action Base de datos antigua No comment provided by engineer. + + On your phone, not on servers. + En tu teléfono, no en algún servidor. + No comment provided by engineer. + One-time invitation link Enlace de invitación de un solo uso No comment provided by engineer. + + One-time link + Enlace de un solo uso + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5746,6 +6290,11 @@ Requiere activación de la VPN. No se usarán hosts .onion. No comment provided by engineer. + + Only channel owners can change channel preferences. + Sólo los propietarios pueden modificar las preferencias de los canales. + No comment provided by engineer. + Only chat owners can change preferences. Sólo los propietarios del chat pueden cambiar las preferencias. @@ -5849,7 +6398,8 @@ Requiere activación de la VPN. Open Abrir - alert action + alert action +alert button Open Settings @@ -5861,6 +6411,11 @@ Requiere activación de la VPN. Abrir cambios No comment provided by engineer. + + Open channel + Abrir canal + new chat action + Open chat Abrir chat @@ -5881,6 +6436,11 @@ Requiere activación de la VPN. Abrir condiciones No comment provided by engineer. + + Open external link? + ¿Abrir enlace externo? + alert title + Open full link Abrir enlace completo @@ -5901,6 +6461,11 @@ Requiere activación de la VPN. Abrir menú migración a otro dispositivo authentication reason + + Open new channel + Abrir canal nuevo + new chat action + Open new chat Abrir chat nuevo @@ -5946,6 +6511,17 @@ Requiere activación de la VPN. Servidor del operador alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + Los operadores se comprometen a: +- Ser independientes +- Minimizar el tratamiento de metadatos +- Ejecutar código open-source verificado + No comment provided by engineer. + Or import archive file O importa desde un archivo @@ -5966,6 +6542,11 @@ Requiere activación de la VPN. O comparte de forma segura este enlace al archivo No comment provided by engineer. + + Or show QR in person or via video call. + O muestra el código QR en persona o por videollamada. + No comment provided by engineer. + Or show this code O muestra este código @@ -5976,6 +6557,11 @@ Requiere activación de la VPN. O para compartir en privado No comment provided by engineer. + + Or use this QR - print or show online. + O usa el QR, imprímelo o muestralo en línea. + No comment provided by engineer. + Organize chats into lists Organiza tus chats en listas @@ -5993,6 +6579,21 @@ Requiere activación de la VPN. %@ alert message + + Owner + Propietario + No comment provided by engineer. + + + Owners + Propietarios + No comment provided by engineer. + + + Ownership: you can run your own relays. + En propiedad: puedes poner en marcha tus propios servidores. + No comment provided by engineer. + PING count Contador PING @@ -6048,6 +6649,11 @@ Requiere activación de la VPN. Pegar imagen No comment provided by engineer. + + Paste link / Scan + Pegar enlace / Escanear + No comment provided by engineer. + Paste link to connect! Pegar enlace para conectar! @@ -6202,6 +6808,16 @@ Error: %@ Conserva el último borrador del mensaje con los datos adjuntos. No comment provided by engineer. + + Preset relay address + Direcciones predefinidas + No comment provided by engineer. + + + Preset relay name + Nombres predefinidos + No comment provided by engineer. + Preset server address Dirección predefinida del servidor @@ -6237,14 +6853,14 @@ Error: %@ Política de privacidad y condiciones de uso. No comment provided by engineer. - - Privacy redefined - Privacidad redefinida + + Privacy: for owners and subscribers. + Privacidad: para propietarios y suscriptores. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. - Los chats privados, los grupos y tus contactos no son accesibles para los operadores de servidores. + + Private and secure messaging. + Mensajería segura y privada. No comment provided by engineer. @@ -6287,6 +6903,11 @@ Error: %@ Timeout enrutamiento privado alert title + + Proceed + Continuar + alert action + Profile and server connections Eliminar perfil y conexiones @@ -6312,9 +6933,9 @@ Error: %@ Tema del perfil No comment provided by engineer. - - Profile update will be sent to your contacts. - La actualización del perfil se enviará a tus contactos. + + Profile update will be sent to your SimpleX contacts. + La actualización del perfil se enviará a tus contactos SimpleX. alert message @@ -6322,6 +6943,11 @@ Error: %@ No se permiten llamadas y videollamadas. No comment provided by engineer. + + Prohibit chats with admins. + El chat con los administradores no está permitido. + No comment provided by engineer. + Prohibit irreversible message deletion. No se permite la eliminación irreversible de mensajes. @@ -6352,6 +6978,11 @@ Error: %@ No se permiten mensajes directos entre miembros. No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + No se permiten mensajes directos entre suscriptores. + No comment provided by engineer. + Prohibit sending disappearing messages. No se permiten mensajes temporales. @@ -6419,6 +7050,11 @@ Actívalo en ajustes de *Servidores y Redes*. El proxy requiere contraseña No comment provided by engineer. + + Public channels - speak freely 🚀 + Canales públicos - habla con libertad 🚀 + No comment provided by engineer. + Push notifications Notificaciones push @@ -6459,24 +7095,14 @@ Actívalo en ajustes de *Servidores y Redes*. Saber más No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - Conoce más en la [Guía del Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + Conoce más en la Guía del Usuario. No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - No comment provided by engineer. - - - Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - Conoce más en el [Manual del Usuario](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). - Conoce más en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme). + + Read more in our GitHub repository. + Conoce más en nuestro repositorio GitHub. No comment provided by engineer. @@ -6636,6 +7262,31 @@ swipe action ¿Rechazar al miembro? alert title + + Relay + Servidor + No comment provided by engineer. + + + Relay address + Dirección del servidor + alert title + + + Relay connection failed + La conexión con el servidor ha fallado + alert title + + + Relay link + Enlace servidor + No comment provided by engineer. + + + Relay results: + Resultados del servidor: + alert message + Relay server is only used if necessary. Another party can observe your IP address. El servidor de retransmisión sólo se usa en caso de necesidad. Un tercero podría ver tu IP. @@ -6646,6 +7297,16 @@ swipe action El servidor de retransmisión protege tu IP pero puede ver la duración de la llamada. No comment provided by engineer. + + Relay test failed! + ¡El test del servidor ha fallado! + No comment provided by engineer. + + + Reliability: many relays per channel. + Fiabilidad: muchos servidores por canal. + No comment provided by engineer. + Remove Eliminar @@ -6686,6 +7347,16 @@ swipe action ¿Eliminar contraseña de Keychain? No comment provided by engineer. + + Remove subscriber + Eliminar suscriptor + No comment provided by engineer. + + + Remove subscriber? + ¿Eliminar suscriptor? + alert title + Removes messages and blocks members. Elimina mensajes y bloquea miembros. @@ -6921,6 +7592,11 @@ swipe action Proxy SOCKS No comment provided by engineer. + + Safe web links + Enlaces web seguros + No comment provided by engineer. + Safely receive files Recibe archivos de forma segura @@ -6947,6 +7623,11 @@ chat item action Guardar (y notificar miembros) alert button + + Save (and notify subscribers) + Guardar (y notificar suscriptores) + alert button + Save admission settings? ¿Guardar configuración? @@ -6962,6 +7643,11 @@ chat item action Guardar y notificar grupo No comment provided by engineer. + + Save and notify subscribers + Guardar y notificar suscriptores + No comment provided by engineer. + Save and reconnect Guardar y reconectar @@ -6972,6 +7658,16 @@ chat item action Guardar y actualizar perfil del grupo No comment provided by engineer. + + Save channel profile + Guardar perfil del canal + No comment provided by engineer. + + + Save channel profile? + ¿Guardar perfil del canal? + alert title + Save group profile Guardar perfil de grupo @@ -7152,6 +7848,11 @@ chat item action Código de seguridad No comment provided by engineer. + + Security: owners hold channel keys. + Seguridad: los propietarios tienen la llave del canal. + No comment provided by engineer. + Select Seleccionar @@ -7282,6 +7983,11 @@ chat item action Enviar solicitud sin mensaje No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + Envía el enlace con cualquier mensajero, es seguro. El contacto debe pegarlo en SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. Envíalos desde la galería o desde teclados personalizados. @@ -7292,6 +7998,11 @@ chat item action Se envían hasta 100 mensajes más recientes a los miembros nuevos. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + Se envían hasta 100 mensajes más recientes a los suscriptores nuevos. + No comment provided by engineer. + Send your private feedback to groups. Envía tu comentario privado a los grupos. @@ -7307,6 +8018,11 @@ chat item action El remitente puede haber eliminado la solicitud de conexión. No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + Enviar una previsualización del enlace puede revelar tu dirección IP al sitio web. Puedes cambiarlo más tarde en los ajustes de privacidad. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. El envío de confirmaciones de entrega se activará para todos los contactos en todos los perfiles visibles. @@ -7432,6 +8148,11 @@ chat item action El protocolo del servidor ha cambiado. alert title + + Server requires authorization to connect to relay, check password. + El servidor requiere autorización para conectar con el servidor, comprueba la contraseña. + relay test error + Server requires authorization to create queues, check password. El servidor requiere autorización para crear colas, comprueba la contraseña. @@ -7562,6 +8283,16 @@ chat item action La configuración ha sido modificada. alert message + + Setup notifications + Configurar notificaciones + No comment provided by engineer. + + + Setup routers + Configurar routers + No comment provided by engineer. + Shape profile images Dar forma a las imágenes de perfil @@ -7598,11 +8329,16 @@ chat item action Campartir dirección públicamente No comment provided by engineer. - - Share address with contacts? - ¿Compartir la dirección con los contactos? + + Share address with SimpleX contacts? + ¿Compartir la dirección con los contactos SimpleX? alert title + + Share channel + Compartir canal + No comment provided by engineer. + Share from other apps. Comparte desde otras aplicaciones. @@ -7628,6 +8364,11 @@ chat item action Perfil a compartir No comment provided by engineer. + + Share relay address + Compartir dirección del servidor + No comment provided by engineer. + Share this 1-time invite link Comparte este enlace de un solo uso @@ -7638,9 +8379,14 @@ chat item action Compartir con Simplex No comment provided by engineer. - - Share with contacts - Compartir con contactos + + Share via chat + Compartir mediante chat + No comment provided by engineer. + + + Share with SimpleX contacts + Compartir con contactos SimpleX No comment provided by engineer. @@ -7813,9 +8559,9 @@ chat item action Protocolos de SimpleX auditados por Trail of Bits. No comment provided by engineer. - - SimpleX relay link - Enlace de servidor SimpleX + + SimpleX relay address + Dirección de servidor SimpleX simplex link type @@ -7891,6 +8637,11 @@ report reason Cuadrada, circular o cualquier forma intermedia. No comment provided by engineer. + + Star on GitHub + Estrella en GitHub + No comment provided by engineer. + Start chat Iniciar chat @@ -7991,6 +8742,78 @@ report reason Suscritas No comment provided by engineer. + + Subscriber + Suscriptor + No comment provided by engineer. + + + Subscriber reports + Informes de suscriptores + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + El suscriptor será eliminado del canal. ¡No puede deshacerse! + alert message + + + Subscribers + Suscriptores + No comment provided by engineer. + + + Subscribers can add message reactions. + Los suscriptores pueden añadir reacciones a los mensajes. + No comment provided by engineer. + + + Subscribers can chat with admins. + Los suscriptores pueden chatear con los administradores. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + Los suscriptores del canal pueden eliminar mensajes de forma irreversible. (24 horas) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + Los suscriptores pueden informar de mensajes a los moderadores. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + Los suscriptores del canal pueden enviar enlaces SimpleX. + No comment provided by engineer. + + + Subscribers can send direct messages. + Los suscriptores del canal pueden enviar mensajes directos. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + Los suscriptores del canal pueden enviar mensajes temporales. + No comment provided by engineer. + + + Subscribers can send files and media. + Los suscriptores del canal pueden enviar archivos y multimedia. + No comment provided by engineer. + + + Subscribers can send voice messages. + Los suscriptores del canal pueden enviar mensajes de voz. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + Los suscriptores usan el enlace del servidor para conectarse a los canales. +La dirección del servidor se usó para establecer el servidor para el canal. + No comment provided by engineer. + Subscription errors Errores de suscripción @@ -8071,6 +8894,11 @@ report reason Hacer foto No comment provided by engineer. + + Talk to someone + Para comunicarte + No comment provided by engineer. + Tap Connect to chat Pulsa Conectar para chatear @@ -8086,9 +8914,9 @@ report reason Pulsa Conectar para usar el bot No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. - Pulsa Crear dirección SimpleX en el menú para crearla más tarde. + + Tap Join channel + Pulsa Unirme al canal No comment provided by engineer. @@ -8121,6 +8949,11 @@ report reason Pulsa para unirte en modo incógnito No comment provided by engineer. + + Tap to open + Pulsa para abrir + No comment provided by engineer. + Tap to paste link Pulsa aquí para pegar el enlace @@ -8139,13 +8972,19 @@ report reason Test failed at step %@. Prueba no superada en el paso %@. - server test failure + relay test failure +server test failure Test notifications Probar notificaciones No comment provided by engineer. + + Test relay + Test servidor + No comment provided by engineer. + Test server Probar servidor @@ -8198,6 +9037,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. La aplicación protege tu privacidad mediante el uso de diferentes operadores en cada conversación. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + La app ha eliminado el mensaje tras %lld intentos de recibirlo. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). La aplicación pedirá que confirmes las descargas desde servidores de archivos desconocidos (excepto si son .onion). @@ -8213,6 +9057,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. El código QR escaneado no es un enlace de SimpleX. No comment provided by engineer. + + The connection reached the limit of undelivered messages + La conexión ha alcanzado al límite de mensajes no entregados + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. La conexión ha alcanzado el límite de mensajes no entregados. es posible que tu contacto esté desconectado. @@ -8238,9 +9087,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. El cifrado funciona y un cifrado nuevo no es necesario. ¡Podría dar lugar a errores de conexión! No comment provided by engineer. - - The future of messaging - La nueva generación de mensajería privada + + The first network where you own +your contacts and groups. + La primera red donde los grupos +y los contactos son tuyos. No comment provided by engineer. @@ -8278,6 +9129,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. La base de datos antigua no se eliminó durante la migración, puede eliminarse. No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + La libertad más antigua del ser humano, la de hablar con otra persona sin ser observado, materializada sobre una infraestructura que no puede traicionarla. + No comment provided by engineer. + The same conditions will apply to operator **%@**. Las mismas condiciones se aplicarán al operador **%@**. @@ -8323,6 +9179,16 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Temas No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + Después pasamos a internet y cada plataforma pedía una parte de tí: tu nombre, tu número, tus amistades. Aceptamos que el precio de hablar con los demás es informar a alguien de quién es interlocutor. Cada generación, personas y tecnología, ha funcionado así: teléfono, email, mensajería, redes sociales. Parecía el único camino. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + Existe otro camino. Una red sin números de teléfono. Sin nombres de usuario. Sin cuentas. Sin identificadores de ningún tipo. Una red que conecta las personas y entrega mensajes cifrados sin saber quien está conectado. + No comment provided by engineer. + These conditions will also apply for: **%@**. Estas condiciones también se aplican para: **%@**. @@ -8388,6 +9254,16 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Este grupo ya no existe. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + Esto es una dirección de servidor, no puede usarse para conectar. + alert message + + + This is your link for channel %@! + Este es tu enlace para el canal %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Este enlace requiere una versión más reciente de la aplicación. Por favor, actualiza la aplicación o pide a tu contacto un enlace compatible. @@ -8438,6 +9314,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Para ocultar mensajes no deseados. No comment provided by engineer. + + To make SimpleX Network last. + Para que la Red SimpleX perdure. + No comment provided by engineer. + To make a new connection Para hacer una conexión nueva @@ -8525,11 +9406,6 @@ Se te pedirá que completes la autenticación antes de activar esta función.Para verificar el cifrado de extremo a extremo con tu contacto, compara (o escanea) el código en ambos dispositivos. No comment provided by engineer. - - Toggle chat list: - Alternar lista de chats: - No comment provided by engineer. - Toggle incognito when connecting. Activa incógnito al conectar. @@ -8545,6 +9421,11 @@ Se te pedirá que completes la autenticación antes de activar esta función.Opacidad barra No comment provided by engineer. + + Top bar + Barra superior + No comment provided by engineer. + Total Total @@ -8610,6 +9491,11 @@ Se te pedirá que completes la autenticación antes de activar esta función.¿Desbloquear miembro? No comment provided by engineer. + + Unblock subscriber for all? + ¿Desbloquear al suscriptor para todos? + No comment provided by engineer. + Undelivered messages Mensajes no entregados @@ -8710,13 +9596,18 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Unsupported connection link Enlace de conexión no compatible - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. Hasta 100 últimos mensajes son enviados a los miembros nuevos. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + Hasta 100 últimos mensajes son enviados a los suscriptores nuevos. + No comment provided by engineer. + Update Actualizar @@ -8842,11 +9733,6 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Usar puerto TCP 443 solo en servidores predefinidos. No comment provided by engineer. - - Use chat - Usar Chat - No comment provided by engineer. - Use current profile Usar perfil actual @@ -8862,6 +9748,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Uso para mensajes No comment provided by engineer. + + Use for new channels + Usar para canales nuevos + No comment provided by engineer. + Use for new connections Para conexiones nuevas @@ -8902,6 +9793,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Usar enrutamiento privado con servidores de mensaje desconocidos. No comment provided by engineer. + + Use relay + Usar servidor + No comment provided by engineer. + Use server Usar servidor @@ -8922,6 +9818,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Usa la aplicación con una sola mano. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + Usa esta dirección en tu perfil de redes sociales, página web o firma email. + No comment provided by engineer. + Use web port Usar puerto web @@ -8942,6 +9843,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Usar servidores SimpleX Chat. No comment provided by engineer. + + Verify + Verificar + relay test step + Verify code with desktop Verificar código con ordenador @@ -9062,6 +9968,21 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Mensaje de voz… No comment provided by engineer. + + Wait + Espera + alert action + + + Wait response + Espera respuesta + relay test step + + + Waiting for channel owner to add relays. + Esperando a que el propietario del canal añada servidores. + No comment provided by engineer. + Waiting for desktop... Esperando ordenador... @@ -9102,6 +10023,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Atención: ¡puedes perder algunos datos! No comment provided by engineer. + + We made connecting simpler for new users. + Hemos simplificado la conexión para los usuarios nuevos. + No comment provided by engineer. + WebRTC ICE servers Servidores WebRTC ICE @@ -9152,6 +10078,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Cuando compartes un perfil incógnito con alguien, este perfil también se usará para los grupos a los que te inviten. No comment provided by engineer. + + Why SimpleX is built. + Por qué fue creado SimpleX. + No comment provided by engineer. + WiFi WiFi @@ -9364,9 +10295,14 @@ Repeat join request? Puedes configurar las notificaciones de la pantalla de bloqueo desde Configuración. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + Puedes compartir un enlace o código QR. Cualquiera podrá unirse al canal. + 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. - Puedes compartir el enlace o el código QR para que cualquiera pueda unirse al grupo. Si más tarde lo eliminas, no afectará a los miembros del grupo. + Puedes compartir el enlace o código QR. Cualquiera podrá unirse al grupo. Si más tarde lo eliminas, no afectará a los miembros del grupo. No comment provided by engineer. @@ -9409,16 +10345,25 @@ Repeat join request? ¡No puedes enviar mensajes! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + Te comprometes a: +- Sólo contenido legal en grupos públicos +- Respetar a los demás usuarios — no hacer spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + Te conectaste al canal mediante este enlace de servidor. + No comment provided by engineer. + You could not be verified; please try again. No has podido ser autenticado. Inténtalo de nuevo. No comment provided by engineer. - - You decide who can connect. - Tu decides quién se conecta. - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -9486,6 +10431,11 @@ Repeat connection request? Deberías recibir notificaciones. token info + + You were born without an account + Naciste sin una cuenta + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. Podrás enviar mensajes **después de que tu solicitud sea aceptada**. @@ -9521,9 +10471,14 @@ Repeat connection request? Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + Dejarás de recibir mensajes de este canal. El historial del chat se conservará. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. - Dejarás de recibir mensajes del chat. El historial del chat se conserva. + Dejarás de recibir mensajes del chat. El historial del chat se conservará. No comment provided by engineer. @@ -9566,6 +10521,11 @@ Repeat connection request? Llamadas No comment provided by engineer. + + Your channel + Tu canal + No comment provided by engineer. + Your chat database Base de datos @@ -9616,6 +10576,11 @@ Repeat connection request? Tus contactos permanecerán conectados. No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + Tus conversaciones te pertenecen, tal como ha sido siempre antes de la llegada de internet. Tu red no es un lugar que visitas. Es un lugar que has creado, te pertenece y nadie te la podrá quitar, ya sea pública o privada. + No comment provided by engineer. + Your credentials may be sent unencrypted. Tus credenciales podrían ser enviadas sin cifrar. @@ -9636,6 +10601,11 @@ Repeat connection request? Mi grupo No comment provided by engineer. + + Your network + Tu red + No comment provided by engineer. + Your preferences Mis preferencias @@ -9651,6 +10621,13 @@ Repeat connection request? Tu perfil No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + El perfil **%@** será compartido con los servidores de canal y los suscriptores. +Los servidores tienen acceso a los mensajes del canal. + No comment provided by engineer. + Your profile **%@** will be shared. El perfil **%@** será compartido. @@ -9671,11 +10648,26 @@ Repeat connection request? Tu perfil ha sido modificado. Si lo guardas la actualización será enviada a todos tus contactos. alert message + + Your public address + Tu dirección pública + No comment provided by engineer. + Your random profile Tu perfil aleatorio No comment provided by engineer. + + Your relay address + Tu dirección de servidor + No comment provided by engineer. + + + Your relay name + Tu nombre del servidor + No comment provided by engineer. + Your server address Dirección del servidor @@ -9691,21 +10683,11 @@ Repeat connection request? Configuración No comment provided by engineer. - - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - [Contribuye](https://github.com/simplex-chat/simplex-chat#contribute) - No comment provided by engineer. - [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) - No comment provided by engineer. - \_italic_ \_italic_ @@ -9721,6 +10703,11 @@ Repeat connection request? y después elige: No comment provided by engineer. + + accepted + aceptado + No comment provided by engineer. + accepted %@ %@ aceptado @@ -9741,6 +10728,11 @@ Repeat connection request? te ha admitido rcv group event chat item + + active + activo + No comment provided by engineer. + admin administrador @@ -9852,6 +10844,11 @@ marked deleted chat item preview text llamando… call status + + can't broadcast + no puedes retransmitir + No comment provided by engineer. + can't send messages no se pueden enviar mensajes @@ -9887,6 +10884,16 @@ marked deleted chat item preview text cambiando de servidor… chat item text + + channel + canal + shown as sender role for channel messages + + + channel profile updated + perfil del canal actualizado + snd group event chat item + colored coloreado @@ -10033,6 +11040,11 @@ pref value eliminado deleted chat item + + deleted channel + canal eliminado + rcv group event chat item + deleted contact contacto eliminado @@ -10143,6 +11155,11 @@ pref value error No comment provided by engineer. + + error: %@ + error: %@ + receive error chat item + expired expirados @@ -10150,6 +11167,7 @@ pref value failed + fallo No comment provided by engineer. @@ -10272,6 +11290,11 @@ pref value ha salido rcv group event chat item + + link + enlace + No comment provided by engineer. + marked deleted marcado eliminado @@ -10342,6 +11365,11 @@ pref value nunca delete after time + + new + nuevo + No comment provided by engineer. + new message mensaje nuevo @@ -10465,6 +11493,11 @@ time to disappear llamada rechazada call status + + relay + servidor + member role + removed expulsado @@ -10475,6 +11508,16 @@ time to disappear ha expulsado a %@ rcv group event chat item + + removed (%d attempts) + eliminado (%d intentos) + receive error chat item + + + removed by operator + eliminado por el operador + No comment provided by engineer. + removed contact address dirección de contacto eliminada @@ -10629,6 +11672,11 @@ last received msg: %2$@ desprotegida No comment provided by engineer. + + updated channel profile + perfil del canal actualizado + rcv group event chat item + updated group profile ha actualizado el perfil del grupo @@ -10649,6 +11697,11 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + mediante %@ + relay hostname + via contact address link mediante enlace de dirección de contacto @@ -10724,6 +11777,11 @@ last received msg: %2$@ Tu rol es observador No comment provided by engineer. + + you are subscriber + eres suscriptor + No comment provided by engineer. + you blocked %@ has bloqueado a %@ @@ -10784,6 +11842,11 @@ last received msg: %2$@ \~strike~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + ⚠️ Verificación de firma fallida: %@. + owner verification + 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 5a7813dfe5..892f686bd2 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -172,6 +172,21 @@ %d kuukautta time interval + + %d relays failed + channel relay bar +channel subscriber relay bar + + + %d relays not active + channel relay bar +channel subscriber relay bar + + + %d relays removed + channel relay bar +channel subscriber relay bar + %d sec %d sek @@ -186,11 +201,53 @@ %d ohitettua viestiä integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d viikkoa time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + channel relay bar + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + channel subscriber relay bar + %lld %lld @@ -201,6 +258,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld kontaktia valittu @@ -296,10 +357,18 @@ %u viestit ohitettu. No comment provided by engineer. + + (from owner) + chat link info line + (new) No comment provided by engineer. + + (signed) + chat link info line + (this device v%@) No comment provided by engineer. @@ -340,6 +409,10 @@ **Scan / Paste link**: to connect via a link you received. No comment provided by engineer. + + **Test relay** to retrieve its name. + 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. @@ -379,6 +452,12 @@ - ja paljon muuta! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -470,6 +549,10 @@ time interval Muutama asia lisää No comment provided by engineer. + + A link for one person to connect + No comment provided by engineer. + A new contact Uusi kontakti @@ -584,9 +667,8 @@ swipe action Active connections 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. + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. @@ -646,6 +728,10 @@ swipe action Added message servers No comment provided by engineer. + + Adding relays will be supported later. + No comment provided by engineer. + Additional accent No comment provided by engineer. @@ -751,6 +837,14 @@ swipe action All profiles profile dropdown + + All relays failed + No comment provided by engineer. + + + All relays removed + No comment provided by engineer. + All reports will be archived for you. No comment provided by engineer. @@ -805,6 +899,10 @@ swipe action Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle. (24 tuntia) No comment provided by engineer. + + Allow members to chat with admins. + No comment provided by engineer. + Allow message reactions only if your contact allows them. Salli reaktiot viesteihin vain, jos kontaktisi sallii ne. @@ -820,6 +918,10 @@ swipe action Salli yksityisviestien lähettäminen jäsenille. No comment provided by engineer. + + Allow sending direct messages to subscribers. + No comment provided by engineer. + Allow sending disappearing messages. Salli katoavien viestien lähettäminen. @@ -829,6 +931,10 @@ swipe action Allow sharing No comment provided by engineer. + + Allow subscribers to chat with admins. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Salli lähetettyjen viestien peruuttamaton poistaminen. (24 tuntia) @@ -927,11 +1033,6 @@ swipe action Vastaa puheluun No comment provided by engineer. - - Anybody can host servers. - Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia. - No comment provided by engineer. - App build: %@ Sovellusversio: %@ @@ -1118,6 +1219,19 @@ swipe action Virheellinen viestin tarkiste No comment provided by engineer. + + Be free +in your network + No comment provided by engineer. + + + Be free in your network. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + No comment provided by engineer. + Better calls No comment provided by engineer. @@ -1195,6 +1309,10 @@ swipe action Block member? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin No comment provided by engineer. @@ -1240,13 +1358,21 @@ swipe action Sekä sinä että kontaktisi voitte lähettää ääniviestejä. No comment provided by engineer. + + Bottom bar + No comment provided by engineer. + + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. Business address - No comment provided by engineer. + chat link info line Business chats @@ -1265,12 +1391,6 @@ swipe action 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. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - No comment provided by engineer. - Call already ended! Puhelu on jo päättynyt! @@ -1407,6 +1527,67 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + alert message +alert subtitle + + + Channel image + No comment provided by engineer. + + + Channel link + chat link info line + + + Channel preferences + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel temporarily unavailable + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + + + Channels + No comment provided by engineer. + Chat No comment provided by engineer. @@ -1483,6 +1664,22 @@ set passcode view Käyttäjäprofiili No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme No comment provided by engineer. @@ -1497,7 +1694,8 @@ set passcode view Chat with admins - chat toolbar + chat feature +chat toolbar Chat with member @@ -1512,10 +1710,22 @@ set passcode view Keskustelut No comment provided by engineer. + + Chats with admins are prohibited. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + alert message + Chats with members No comment provided by engineer. + + Chats with members are disabled + No comment provided by engineer. + Check messages every 20 min. No comment provided by engineer. @@ -1524,6 +1734,14 @@ set passcode view Check messages when allowed. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Tarkista palvelimen osoite ja yritä uudelleen. @@ -1651,8 +1869,8 @@ set passcode view Määritä ICE-palvelimet No comment provided by engineer. - - Configure server operators + + Configure relays No comment provided by engineer. @@ -1707,7 +1925,8 @@ set passcode view Connect Yhdistä - server test step + relay test step +server test step Connect automatically @@ -1744,6 +1963,10 @@ This is your own one-time link! Yhdistä linkin kautta new chat sheet title + + Connect via link or QR code + No comment provided by engineer. + Connect via one-time link Yhdistä kertalinkillä @@ -1812,7 +2035,7 @@ This is your own one-time link! Connection error (AUTH) Yhteysvirhe (AUTH) - No comment provided by engineer. + conn error description Connection failed @@ -1861,6 +2084,10 @@ This is your own one-time link! Connections No comment provided by engineer. + + Contact address + chat link info line + Contact allows Kontakti sallii @@ -1926,6 +2153,11 @@ This is your own one-time link! Jatka No comment provided by engineer. + + Contribute + Osallistu + No comment provided by engineer. + Conversation deleted! No comment provided by engineer. @@ -1950,12 +2182,7 @@ This is your own one-time link! Correct name to %@? - No comment provided by engineer. - - - Create - Luo - No comment provided by engineer. + alert message Create 1-time link @@ -2003,6 +2230,14 @@ This is your own one-time link! Luo profiilisi No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Luo jono @@ -2012,11 +2247,19 @@ This is your own one-time link! Create your address No comment provided by engineer. + + Create your link + No comment provided by engineer. + Create your profile Luo profiilisi No comment provided by engineer. + + Create your public address + No comment provided by engineer. + Created No comment provided by engineer. @@ -2033,6 +2276,10 @@ This is your own one-time link! Creating archive link No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… No comment provided by engineer. @@ -2184,10 +2431,9 @@ This is your own one-time link! Debug delivery No comment provided by engineer. - - Decentralized - Hajautettu - No comment provided by engineer. + + Decode link + relay test step Decryption error @@ -2232,6 +2478,14 @@ swipe action Delete and notify contact No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat No comment provided by engineer. @@ -2393,6 +2647,10 @@ alert button Poista jono server test step + + Delete relay + No comment provided by engineer. + Delete report No comment provided by engineer. @@ -2540,6 +2798,14 @@ alert button Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä. No comment provided by engineer. + + Direct messages between subscribers are prohibited. + No comment provided by engineer. + + + Disable + alert button + Disable (keep overrides) Poista käytöstä (pidä ohitukset) @@ -2637,6 +2903,10 @@ alert button Do not send history to new members. No comment provided by engineer. + + Do not send history to new subscribers. + No comment provided by engineer. + Do not use credentials with proxy. No comment provided by engineer. @@ -2725,11 +2995,19 @@ chat item action E2E encrypted notifications. No comment provided by engineer. + + Easier to invite your friends 👋 + No comment provided by engineer. + Edit Muokkaa chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Muokkaa ryhmäprofiilia @@ -2742,7 +3020,7 @@ chat item action Enable Salli - No comment provided by engineer. + alert button Enable (keep overrides) @@ -2763,6 +3041,10 @@ chat item action Ota TCP-säilytys käyttöön No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Ota automaattinen viestien poisto käyttöön? @@ -2772,6 +3054,10 @@ chat item action Enable camera access No comment provided by engineer. + + Enable chats with admins? + alert title + Enable disappearing messages by default. No comment provided by engineer. @@ -2790,16 +3076,15 @@ chat item action Salli välittömät ilmoitukset? No comment provided by engineer. + + Enable link previews? + alert title + 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? @@ -2898,6 +3183,10 @@ chat item action Syötä pääsykoodi No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Anna oikea tunnuslause. @@ -2921,6 +3210,14 @@ chat item action Kirjoita yllä oleva salasana näyttääksesi! No comment provided by engineer. + + Enter profile name... + No comment provided by engineer. + + + Enter relay name… + No comment provided by engineer. + Enter server manually Syötä palvelin manuaalisesti @@ -2947,7 +3244,7 @@ chat item action Error Virhe - No comment provided by engineer. + conn error description Error aborting address change @@ -2972,6 +3269,10 @@ chat item action Virhe lisättäessä jäseniä No comment provided by engineer. + + Error adding relay + alert title + Error adding server alert title @@ -3024,6 +3325,10 @@ chat item action Virhe osoitteen luomisessa No comment provided by engineer. + + Error creating channel + alert title + Error creating group Virhe ryhmän luomisessa @@ -3149,10 +3454,6 @@ chat item action Error opening chat No comment provided by engineer. - - Error opening group - No comment provided by engineer. - Error receiving file Virhe tiedoston vastaanottamisessa @@ -3192,6 +3493,10 @@ chat item action Virhe ICE-palvelimien tallentamisessa No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list alert title @@ -3251,6 +3556,10 @@ chat item action Virhe toimituskuittauksien asettamisessa! No comment provided by engineer. + + Error sharing channel + alert title + Error starting chat Virhe käynnistettäessä keskustelua @@ -3325,7 +3634,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3542,7 +3852,8 @@ snd error text Fingerprint in server address does not match certificate. Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3582,9 +3893,14 @@ snd error text For all moderators No comment provided by engineer. + + For anyone to reach you + No comment provided by engineer. + For chat profile %@: - servers error + servers error +servers warning For console @@ -3703,10 +4019,18 @@ Error: %2$@ GIFit ja tarrat No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. No comment provided by engineer. + + Get started + No comment provided by engineer. + Good afternoon! message preview @@ -3761,7 +4085,7 @@ Error: %2$@ Group link Ryhmälinkki - No comment provided by engineer. + chat link info line Group links @@ -3869,6 +4193,10 @@ Error: %2$@ History is not sent to new members. No comment provided by engineer. + + History is not sent to new subscribers. + No comment provided by engineer. + How SimpleX works Miten SimpleX toimii @@ -3962,11 +4290,6 @@ Error: %2$@ Heti No comment provided by engineer. - - Immune to spam - Immuuni roskapostille ja väärinkäytöksille - No comment provided by engineer. - Import Tuo @@ -4097,9 +4420,9 @@ More improvements are coming soon! 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) + + Install SimpleX Chat for terminal + Asenna SimpleX Chat terminaalille No comment provided by engineer. @@ -4150,7 +4473,7 @@ More improvements are coming soon! Invalid connection link Virheellinen yhteyslinkki - No comment provided by engineer. + conn error description Invalid display name! @@ -4166,7 +4489,15 @@ More improvements are coming soon! Invalid name! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4201,6 +4532,10 @@ More improvements are coming soon! Kutsu jäseniä No comment provided by engineer. + + Invite someone privately + No comment provided by engineer. + Invite to chat No comment provided by engineer. @@ -4275,6 +4610,10 @@ More improvements are coming soon! Liity %@:nä No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Liity ryhmään @@ -4354,6 +4693,14 @@ This is your link for group %@! Poistu swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat No comment provided by engineer. @@ -4376,6 +4723,10 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let someone connect to you + No comment provided by engineer. + Let's talk in SimpleX Chat Jutellaan SimpleX Chatissa @@ -4395,6 +4746,10 @@ This is your link for group %@! Link mobile and desktop apps! 🔗 No comment provided by engineer. + + Link signature verified. + owner verification + Linked desktop options No comment provided by engineer. @@ -4562,6 +4917,10 @@ This is your link for group %@! Ryhmän jäsenet voivat lisätä viestireaktioita. No comment provided by engineer. + + Members can chat with admins. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia) @@ -4622,6 +4981,10 @@ This is your link for group %@! Viestiluonnos No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded item status text @@ -4704,6 +5067,14 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + E2EE info chat item + Messages in this chat will never be deleted. alert message @@ -4728,12 +5099,12 @@ This is your link for group %@! Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery. No comment provided by engineer. - - Migrate device + + Migrate No comment provided by engineer. - - Migrate from another device + + Migrate device No comment provided by engineer. @@ -4847,6 +5218,10 @@ This is your link for group %@! Verkko ja palvelimet No comment provided by engineer. + + Network commitments + No comment provided by engineer. + Network connection No comment provided by engineer. @@ -4855,6 +5230,10 @@ This is your link for group %@! Network decentralization No comment provided by engineer. + + Network error + conn error description + Network issues - message expired after many attempts to send it. snd error text @@ -4867,6 +5246,11 @@ This is your link for group %@! Network operator No comment provided by engineer. + + Network routers cannot know +who talks to whom + No comment provided by engineer. + Network settings Verkkoasetukset @@ -4881,6 +5265,10 @@ This is your link for group %@! New token status text + + New 1-time link + No comment provided by engineer. + New Passcode Uusi pääsykoodi @@ -4902,6 +5290,10 @@ This is your link for group %@! New chat experience 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Uusi kontaktipyyntö @@ -4966,11 +5358,28 @@ This is your link for group %@! Ei No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + No comment provided by engineer. + + + No active relays + No comment provided by engineer. + No app password Ei sovelluksen salasanaa Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats No comment provided by engineer. @@ -5097,11 +5506,22 @@ This is your link for group %@! No unread chats No comment provided by engineer. - - No user identifiers. - Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi. + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. + + Non-profit governance + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + No comment provided by engineer. + + + Not all relays connected + alert title + Not compatible! No comment provided by engineer. @@ -5151,7 +5571,7 @@ This is your link for group %@! OK - No comment provided by engineer. + alert button Off @@ -5170,11 +5590,19 @@ new chat action Vanha tietokanta No comment provided by engineer. + + On your phone, not on servers. + No comment provided by engineer. + One-time invitation link Kertakutsulinkki No comment provided by engineer. + + One-time link + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5194,6 +5622,10 @@ Edellyttää VPN:n sallimista. Onion-isäntiä ei käytetä. No comment provided by engineer. + + Only channel owners can change channel preferences. + No comment provided by engineer. + Only chat owners can change preferences. No comment provided by engineer. @@ -5290,7 +5722,8 @@ Edellyttää VPN:n sallimista. Open - alert action + alert action +alert button Open Settings @@ -5301,6 +5734,10 @@ Edellyttää VPN:n sallimista. Open changes No comment provided by engineer. + + Open channel + new chat action + Open chat Avaa keskustelu @@ -5319,6 +5756,10 @@ Edellyttää VPN:n sallimista. Open conditions No comment provided by engineer. + + Open external link? + alert title + Open full link alert action @@ -5335,6 +5776,10 @@ Edellyttää VPN:n sallimista. Open migration to another device authentication reason + + Open new channel + new chat action + Open new chat new chat action @@ -5371,6 +5816,13 @@ Edellyttää VPN:n sallimista. Operator server alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + No comment provided by engineer. + Or import archive file No comment provided by engineer. @@ -5387,6 +5839,10 @@ Edellyttää VPN:n sallimista. Or securely share this file link No comment provided by engineer. + + Or show QR in person or via video call. + No comment provided by engineer. + Or show this code No comment provided by engineer. @@ -5395,6 +5851,10 @@ Edellyttää VPN:n sallimista. Or to share privately No comment provided by engineer. + + Or use this QR - print or show online. + No comment provided by engineer. + Organize chats into lists No comment provided by engineer. @@ -5408,6 +5868,18 @@ Edellyttää VPN:n sallimista. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + + + Ownership: you can run your own relays. + No comment provided by engineer. + PING count PING-määrä @@ -5461,6 +5933,10 @@ Edellyttää VPN:n sallimista. Liitä kuva No comment provided by engineer. + + Paste link / Scan + No comment provided by engineer. + Paste link to connect! No comment provided by engineer. @@ -5599,6 +6075,14 @@ Error: %@ Säilytä viimeinen viestiluonnos liitteineen. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Esiasetettu palvelimen osoite @@ -5630,13 +6114,12 @@ Error: %@ Privacy policy and conditions of use. No comment provided by engineer. - - Privacy redefined - Yksityisyys uudelleen määritettynä + + Privacy: for owners and subscribers. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. + + Private and secure messaging. No comment provided by engineer. @@ -5672,6 +6155,10 @@ Error: %@ Private routing timeout alert title + + Proceed + alert action + Profile and server connections Profiili- ja palvelinyhteydet @@ -5695,9 +6182,8 @@ Error: %@ Profile theme No comment provided by engineer. - - Profile update will be sent to your contacts. - Profiilipäivitys lähetetään kontakteillesi. + + Profile update will be sent to your SimpleX contacts. alert message @@ -5705,6 +6191,10 @@ Error: %@ Estä ääni- ja videopuhelut. No comment provided by engineer. + + Prohibit chats with admins. + No comment provided by engineer. + Prohibit irreversible message deletion. Estä peruuttamaton viestien poistaminen. @@ -5733,6 +6223,10 @@ Error: %@ Estä suorien viestien lähettäminen jäsenille. No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + No comment provided by engineer. + Prohibit sending disappearing messages. Estä katoavien viestien lähettäminen. @@ -5793,6 +6287,10 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. + + Public channels - speak freely 🚀 + No comment provided by engineer. + Push notifications Push-ilmoitukset @@ -5830,23 +6328,14 @@ Enable in *Network & servers* settings. Lue lisää No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + Lue lisää Käyttöoppaasta. No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - 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). + + Read more in our GitHub repository. + Lue lisää GitHub-arkistosta. No comment provided by engineer. @@ -5991,6 +6480,26 @@ swipe action Reject member? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + + + Relay results: + alert message + 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. @@ -6001,6 +6510,14 @@ swipe action Välityspalvelin suojaa IP-osoitteesi, mutta se voi tarkkailla puhelun kestoa. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + + + Reliability: many relays per channel. + No comment provided by engineer. + Remove Poista @@ -6037,6 +6554,14 @@ swipe action Poista tunnuslause avainnipusta? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. No comment provided by engineer. @@ -6245,6 +6770,10 @@ swipe action SOCKS proxy No comment provided by engineer. + + Safe web links + No comment provided by engineer. + Safely receive files No comment provided by engineer. @@ -6268,6 +6797,10 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + alert button + Save admission settings? alert title @@ -6282,6 +6815,10 @@ chat item action Tallenna ja ilmoita ryhmän jäsenille No comment provided by engineer. + + Save and notify subscribers + No comment provided by engineer. + Save and reconnect No comment provided by engineer. @@ -6291,6 +6828,14 @@ chat item action Tallenna ja päivitä ryhmäprofiili No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Tallenna ryhmäprofiili @@ -6452,6 +6997,10 @@ chat item action Turvakoodi No comment provided by engineer. + + Security: owners hold channel keys. + No comment provided by engineer. + Select Valitse @@ -6570,6 +7119,10 @@ chat item action Send request without message No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. Lähetä ne galleriasta tai mukautetuista näppäimistöistä. @@ -6579,6 +7132,10 @@ chat item action Send up to 100 last messages to new members. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + No comment provided by engineer. + Send your private feedback to groups. No comment provided by engineer. @@ -6593,6 +7150,10 @@ chat item action Lähettäjä on saattanut poistaa yhteyspyynnön. No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + alert message + 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. @@ -6705,6 +7266,10 @@ chat item action Server protocol changed. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana @@ -6822,6 +7387,14 @@ chat item action Settings were changed. alert message + + Setup notifications + No comment provided by engineer. + + + Setup routers + No comment provided by engineer. + Shape profile images No comment provided by engineer. @@ -6854,11 +7427,14 @@ chat item action Share address publicly No comment provided by engineer. - - Share address with contacts? - Jaa osoite kontakteille? + + Share address with SimpleX contacts? alert title + + Share channel + No comment provided by engineer. + Share from other apps. No comment provided by engineer. @@ -6880,6 +7456,10 @@ chat item action Share profile No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. @@ -6888,9 +7468,12 @@ chat item action Share to SimpleX No comment provided by engineer. - - Share with contacts - Jaa kontaktien kanssa + + Share via chat + No comment provided by engineer. + + + Share with SimpleX contacts No comment provided by engineer. @@ -7046,8 +7629,8 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7113,6 +7696,11 @@ report reason Square, circle, or anything in between. No comment provided by engineer. + + Star on GitHub + Tähti GitHubissa + No comment provided by engineer. + Start chat Aloita keskustelu @@ -7205,6 +7793,63 @@ report reason Subscribed No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber reports + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers can add message reactions. + No comment provided by engineer. + + + Subscribers can chat with admins. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + No comment provided by engineer. + + + Subscribers can send direct messages. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + No comment provided by engineer. + + + Subscribers can send files and media. + No comment provided by engineer. + + + Subscribers can send voice messages. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors No comment provided by engineer. @@ -7277,6 +7922,10 @@ report reason Ota kuva No comment provided by engineer. + + Talk to someone + No comment provided by engineer. + Tap Connect to chat No comment provided by engineer. @@ -7289,8 +7938,8 @@ report reason Tap Connect to use bot No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. + + Tap Join channel No comment provided by engineer. @@ -7321,6 +7970,10 @@ report reason Napauta liittyäksesi incognito-tilassa No comment provided by engineer. + + Tap to open + No comment provided by engineer. + Tap to paste link No comment provided by engineer. @@ -7336,12 +7989,17 @@ report reason Test failed at step %@. Testi epäonnistui vaiheessa %@. - server test failure + relay test failure +server test failure Test notifications No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Testipalvelin @@ -7392,6 +8050,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.The app protects your privacy by using different operators in each conversation. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -7405,6 +8067,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. No comment provided by engineer. @@ -7429,9 +8095,9 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Salaus toimii ja uutta salaussopimusta ei tarvita. Tämä voi johtaa yhteysvirheisiin! No comment provided by engineer. - - The future of messaging - Seuraavan sukupolven yksityisviestit + + The first network where you own +your contacts and groups. No comment provided by engineer. @@ -7466,6 +8132,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa. No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + No comment provided by engineer. + The same conditions will apply to operator **%@**. No comment provided by engineer. @@ -7505,6 +8175,14 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Themes No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + No comment provided by engineer. + These conditions will also apply for: **%@**. No comment provided by engineer. @@ -7564,6 +8242,14 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Tätä ryhmää ei enää ole olemassa. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -7607,6 +8293,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.To hide unwanted messages. No comment provided by engineer. + + To make SimpleX Network last. + No comment provided by engineer. + To make a new connection Uuden yhteyden luominen @@ -7685,10 +8375,6 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Voit tarkistaa päästä päähän -salauksen kontaktisi kanssa vertaamalla (tai skannaamalla) laitteidenne koodia. No comment provided by engineer. - - Toggle chat list: - No comment provided by engineer. - Toggle incognito when connecting. No comment provided by engineer. @@ -7701,6 +8387,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Toolbar opacity No comment provided by engineer. + + Top bar + No comment provided by engineer. + Total No comment provided by engineer. @@ -7757,6 +8447,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Unblock member? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -7852,12 +8546,16 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Unsupported connection link - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + No comment provided by engineer. + Update Päivitä @@ -7966,11 +8664,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use TCP port 443 for preset servers only. No comment provided by engineer. - - Use chat - Käytä chattia - No comment provided by engineer. - Use current profile Käytä nykyistä profiilia @@ -7984,6 +8677,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use for messages No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Käytä uusiin yhteyksiin @@ -8019,6 +8716,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use private routing with unknown servers. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Käytä palvelinta @@ -8036,6 +8737,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use the app with one hand. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + No comment provided by engineer. + Use web port No comment provided by engineer. @@ -8053,6 +8758,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Käyttää SimpleX Chat -palvelimia. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop No comment provided by engineer. @@ -8162,6 +8871,18 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Ääniviesti… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + + + Waiting for channel owner to add relays. + No comment provided by engineer. + Waiting for desktop... No comment provided by engineer. @@ -8198,6 +8919,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Varoitus: saatat menettää joitain tietoja! No comment provided by engineer. + + We made connecting simpler for new users. + No comment provided by engineer. + WebRTC ICE servers WebRTC ICE -palvelimet @@ -8244,6 +8969,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Kun jaat inkognitoprofiilin jonkun kanssa, tätä profiilia käytetään ryhmissä, joihin tämä sinut kutsuu. No comment provided by engineer. + + Why SimpleX is built. + No comment provided by engineer. + WiFi No comment provided by engineer. @@ -8428,6 +9157,10 @@ Repeat join request? 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 channel. + 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. @@ -8470,16 +9203,21 @@ Repeat join request? Et voi lähettää viestejä! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + 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 decide who can connect. - Kimin bağlanabileceğine siz karar verirsiniz. - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -8541,6 +9279,10 @@ Repeat connection request? You should receive notifications. token info + + You were born without an account + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. No comment provided by engineer. @@ -8574,6 +9316,10 @@ Repeat connection request? Saat edelleen puheluita ja ilmoituksia mykistetyiltä profiileilta, kun ne ovat aktiivisia. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. No comment provided by engineer. @@ -8617,6 +9363,10 @@ Repeat connection request? Puhelusi No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Keskustelut-tietokantasi @@ -8663,6 +9413,10 @@ Repeat connection request? Kontaktisi pysyvät yhdistettyinä. No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + No comment provided by engineer. + Your credentials may be sent unencrypted. No comment provided by engineer. @@ -8681,6 +9435,10 @@ Repeat connection request? Your group No comment provided by engineer. + + Your network + No comment provided by engineer. + Your preferences Asetuksesi @@ -8695,6 +9453,11 @@ Repeat connection request? Your profile No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Profiilisi **%@** jaetaan. @@ -8714,11 +9477,23 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. alert message + + Your public address + No comment provided by engineer. + Your random profile Satunnainen profiilisi No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Palvelimesi osoite @@ -8733,21 +9508,11 @@ Repeat connection request? Asetuksesi 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. - \_italic_ \_italic_ @@ -8763,6 +9528,10 @@ Repeat connection request? edellä, valitse sitten: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ rcv group event chat item @@ -8780,6 +9549,10 @@ Repeat connection request? accepted you rcv group event chat item + + active + No comment provided by engineer. + admin ylläpitäjä @@ -8880,6 +9653,10 @@ marked deleted chat item preview text soittaa… call status + + can't broadcast + No comment provided by engineer. + can't send messages No comment provided by engineer. @@ -8914,6 +9691,14 @@ marked deleted chat item preview text muuttamassa osoitetta… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored värillinen @@ -9054,6 +9839,10 @@ pref value poistettu deleted chat item + + deleted channel + rcv group event chat item + deleted contact rcv direct event chat item @@ -9162,6 +9951,10 @@ pref value virhe No comment provided by engineer. + + error: %@ + receive error chat item + expired No comment provided by engineer. @@ -9285,6 +10078,10 @@ pref value poistunut rcv group event chat item + + link + No comment provided by engineer. + marked deleted merkitty poistetuksi @@ -9351,6 +10148,10 @@ pref value ei koskaan delete after time + + new + No comment provided by engineer. + new message uusi viesti @@ -9464,6 +10265,10 @@ time to disappear hylätty puhelu call status + + relay + member role + removed poistettu @@ -9474,6 +10279,14 @@ time to disappear %@ poistettu rcv group event chat item + + removed (%d attempts) + receive error chat item + + + removed by operator + No comment provided by engineer. + removed contact address profile update event chat item @@ -9605,6 +10418,10 @@ last received msg: %2$@ unprotected No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile päivitetty ryhmäprofiili @@ -9623,6 +10440,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link kontaktiosoitelinkillä @@ -9694,6 +10515,10 @@ last received msg: %2$@ olet tarkkailija No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ snd group event chat item @@ -9752,6 +10577,10 @@ last received msg: %2$@ \~strike~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + owner verification + 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 7e386fe50c..be6a766ca1 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -185,6 +185,21 @@ %d mois time interval + + %d relays failed + channel relay bar +channel subscriber relay bar + + + %d relays not active + channel relay bar +channel subscriber relay bar + + + %d relays removed + channel relay bar +channel subscriber relay bar + %d sec %d sec @@ -200,11 +215,53 @@ %d message·s sauté·s integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d semaines time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + channel relay bar + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + channel subscriber relay bar + %lld %lld @@ -215,6 +272,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld contact·s sélectionné·s @@ -315,11 +376,19 @@ %u messages sautés. No comment provided by engineer. + + (from owner) + chat link info line + (new) (nouveau) No comment provided by engineer. + + (signed) + chat link info line + (this device v%@) (cet appareil v%@) @@ -365,6 +434,10 @@ **Scanner / Coller** : pour vous connecter via un lien que vous avez reçu. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Avertissement** : les notifications push instantanées nécessitent une phrase secrète enregistrée dans la keychain. @@ -408,6 +481,12 @@ - et bien d'autres choses encore ! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -506,6 +585,10 @@ time interval Encore quelques points No comment provided by engineer. + + A link for one person to connect + No comment provided by engineer. + A new contact Un nouveau contact @@ -632,9 +715,8 @@ swipe action Connections actives 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. - 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. + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. @@ -702,6 +784,10 @@ swipe action Ajout de serveurs de messages No comment provided by engineer. + + Adding relays will be supported later. + No comment provided by engineer. + Additional accent Accent additionnel @@ -821,6 +907,14 @@ swipe action Tous les profiles profile dropdown + + All relays failed + No comment provided by engineer. + + + All relays removed + No comment provided by engineer. + All reports will be archived for you. Tous les rapports seront archivés pour vous. @@ -881,6 +975,10 @@ swipe action Autoriser la suppression irréversible des messages uniquement si votre contact vous l'autorise. (24 heures) No comment provided by engineer. + + Allow members to chat with admins. + No comment provided by engineer. + Allow message reactions only if your contact allows them. Autoriser les réactions aux messages uniquement si votre contact les autorise. @@ -896,6 +994,10 @@ swipe action Autoriser l'envoi de messages directs aux membres. No comment provided by engineer. + + Allow sending direct messages to subscribers. + No comment provided by engineer. + Allow sending disappearing messages. Autorise l’envoi de messages éphémères. @@ -906,6 +1008,10 @@ swipe action Autoriser le partage No comment provided by engineer. + + Allow subscribers to chat with admins. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Autoriser la suppression irréversible de messages envoyés. (24 heures) @@ -1011,11 +1117,6 @@ swipe action Répondre à l'appel No comment provided by engineer. - - Anybody can host servers. - N'importe qui peut heberger un serveur. - No comment provided by engineer. - App build: %@ Build de l'app : %@ @@ -1219,6 +1320,19 @@ swipe action Mauvais hash de message No comment provided by engineer. + + Be free +in your network + No comment provided by engineer. + + + Be free in your network. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + No comment provided by engineer. + Better calls Appels améliorés @@ -1312,6 +1426,10 @@ swipe action Bloquer ce membre ? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Bloqué par l'administrateur @@ -1360,6 +1478,14 @@ swipe action Vous et votre contact êtes tous deux en mesure d'envoyer des messages vocaux. No comment provided by engineer. + + Bottom bar + No comment provided by engineer. + + + Broadcast + compose placeholder for channel owner + 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) ! @@ -1368,7 +1494,7 @@ swipe action Business address Adresse professionnelle - No comment provided by engineer. + chat link info line Business chats @@ -1389,15 +1515,6 @@ swipe action 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). No comment provided by engineer. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - En utilisant SimpleX Chat, vous acceptez de : -- n'envoyer que du contenu légal dans les groupes publics. -- respecter les autres utilisateurs - pas de spam. - No comment provided by engineer. - Call already ended! Appel déjà terminé ! @@ -1545,6 +1662,67 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + alert message +alert subtitle + + + Channel image + No comment provided by engineer. + + + Channel link + chat link info line + + + Channel preferences + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel temporarily unavailable + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + + + Channels + No comment provided by engineer. + Chat Discussions @@ -1630,6 +1808,22 @@ set passcode view Profil d'utilisateur No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Thème de chat @@ -1647,7 +1841,8 @@ set passcode view Chat with admins - chat toolbar + chat feature +chat toolbar Chat with member @@ -1662,10 +1857,22 @@ set passcode view Discussions No comment provided by engineer. + + Chats with admins are prohibited. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + alert message + Chats with members No comment provided by engineer. + + Chats with members are disabled + No comment provided by engineer. + Check messages every 20 min. Consulter les messages toutes les 20 minutes. @@ -1676,6 +1883,14 @@ set passcode view Consulter les messages quand c'est possible. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Vérifiez l'adresse du serveur et réessayez. @@ -1821,9 +2036,8 @@ set passcode view Configurer les serveurs ICE No comment provided by engineer. - - Configure server operators - Configurer les opérateurs de serveur + + Configure relays No comment provided by engineer. @@ -1884,7 +2098,8 @@ set passcode view Connect Se connecter - server test step + relay test step +server test step Connect automatically @@ -1929,6 +2144,10 @@ Il s'agit de votre propre lien unique ! Se connecter via un lien new chat sheet title + + Connect via link or QR code + No comment provided by engineer. + Connect via one-time link Se connecter via un lien unique @@ -2007,7 +2226,7 @@ Il s'agit de votre propre lien unique ! Connection error (AUTH) Erreur de connexion (AUTH) - No comment provided by engineer. + conn error description Connection failed @@ -2065,6 +2284,10 @@ Il s'agit de votre propre lien unique ! Connexions No comment provided by engineer. + + Contact address + chat link info line + Contact allows Votre contact autorise @@ -2134,6 +2357,11 @@ Il s'agit de votre propre lien unique ! Continuer No comment provided by engineer. + + Contribute + Contribuer + No comment provided by engineer. + Conversation deleted! Conversation supprimée ! @@ -2162,12 +2390,7 @@ Il s'agit de votre propre lien unique ! Correct name to %@? Corriger le nom pour %@ ? - No comment provided by engineer. - - - Create - Créer - No comment provided by engineer. + alert message Create 1-time link @@ -2219,6 +2442,14 @@ Il s'agit de votre propre lien unique ! Créer le profil No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Créer une file d'attente @@ -2228,11 +2459,19 @@ Il s'agit de votre propre lien unique ! Create your address No comment provided by engineer. + + Create your link + No comment provided by engineer. + Create your profile Créez votre profil No comment provided by engineer. + + Create your public address + No comment provided by engineer. + Created Créées @@ -2253,6 +2492,10 @@ Il s'agit de votre propre lien unique ! Création d'un lien d'archive No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Création d'un lien… @@ -2411,10 +2654,9 @@ Il s'agit de votre propre lien unique ! Livraison de débogage No comment provided by engineer. - - Decentralized - Décentralisé - No comment provided by engineer. + + Decode link + relay test step Decryption error @@ -2462,6 +2704,14 @@ swipe action Supprimer et en informer le contact No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Supprimer la discussion @@ -2630,6 +2880,10 @@ alert button Supprimer la file d'attente server test step + + Delete relay + No comment provided by engineer. + Delete report Supprimer le rapport @@ -2793,6 +3047,14 @@ alert button Les messages directs entre membres sont interdits dans ce groupe. No comment provided by engineer. + + Direct messages between subscribers are prohibited. + No comment provided by engineer. + + + Disable + alert button + Disable (keep overrides) Désactiver (conserver les remplacements) @@ -2898,6 +3160,10 @@ alert button Ne pas envoyer d'historique aux nouveaux membres. No comment provided by engineer. + + Do not send history to new subscribers. + No comment provided by engineer. + Do not use credentials with proxy. Ne pas utiliser d'identifiants avec le proxy. @@ -2999,11 +3265,19 @@ chat item action Notifications chiffrées E2E. No comment provided by engineer. + + Easier to invite your friends 👋 + No comment provided by engineer. + Edit Modifier chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Modifier le profil du groupe @@ -3016,7 +3290,7 @@ chat item action Enable Activer - No comment provided by engineer. + alert button Enable (keep overrides) @@ -3038,6 +3312,10 @@ chat item action Activer le TCP keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Activer la suppression automatique des messages ? @@ -3048,6 +3326,10 @@ chat item action Autoriser l'accès à la caméra No comment provided by engineer. + + Enable chats with admins? + alert title + Enable disappearing messages by default. No comment provided by engineer. @@ -3067,16 +3349,15 @@ chat item action Activer les notifications instantanées ? No comment provided by engineer. + + Enable link previews? + alert title + Enable lock Activer le verrouillage No comment provided by engineer. - - Enable notifications - Activer les notifications - No comment provided by engineer. - Enable periodic notifications? Activer les notifications périodiques ? @@ -3182,6 +3463,10 @@ chat item action Entrer le code d'accès No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Entrez la phrase secrète correcte. @@ -3207,6 +3492,14 @@ chat item action Entrez ci-dessus le mot de passe pour afficher le profil ! No comment provided by engineer. + + Enter profile name... + No comment provided by engineer. + + + Enter relay name… + No comment provided by engineer. + Enter server manually Entrer un serveur manuellement @@ -3235,7 +3528,7 @@ chat item action Error Erreur - No comment provided by engineer. + conn error description Error aborting address change @@ -3261,6 +3554,10 @@ chat item action Erreur lors de l'ajout de membre·s No comment provided by engineer. + + Error adding relay + alert title + Error adding server Erreur lors de l'ajout du serveur @@ -3318,6 +3615,10 @@ chat item action Erreur lors de la création de l'adresse No comment provided by engineer. + + Error creating channel + alert title + Error creating group Erreur lors de la création du groupe @@ -3452,10 +3753,6 @@ chat item action Erreur lors de l'ouverture du chat No comment provided by engineer. - - Error opening group - No comment provided by engineer. - Error receiving file Erreur lors de la réception du fichier @@ -3500,6 +3797,10 @@ chat item action Erreur lors de la sauvegarde des serveurs ICE No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Erreur lors de l'enregistrement de la liste des chats @@ -3564,6 +3865,10 @@ chat item action Erreur lors de la configuration des accusés de réception ! No comment provided by engineer. + + Error sharing channel + alert title + Error starting chat Erreur lors du démarrage du chat @@ -3643,7 +3948,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3883,7 +4189,8 @@ snd error text Fingerprint in server address does not match certificate. Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3923,10 +4230,15 @@ snd error text For all moderators No comment provided by engineer. + + For anyone to reach you + No comment provided by engineer. + For chat profile %@: Pour le profil de discussion %@ : - servers error + servers error +servers warning For console @@ -4066,10 +4378,18 @@ Erreur : %2$@ GIFs et stickers No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. No comment provided by engineer. + + Get started + No comment provided by engineer. + Good afternoon! Bonjour ! @@ -4128,7 +4448,7 @@ Erreur : %2$@ Group link Lien du groupe - No comment provided by engineer. + chat link info line Group links @@ -4237,6 +4557,10 @@ Erreur : %2$@ L'historique n'est pas envoyé aux nouveaux membres. No comment provided by engineer. + + History is not sent to new subscribers. + No comment provided by engineer. + How SimpleX works Comment SimpleX fonctionne @@ -4334,11 +4658,6 @@ Erreur : %2$@ Immédiatement No comment provided by engineer. - - Immune to spam - Protégé du spam et des abus - No comment provided by engineer. - Import Importer @@ -4479,9 +4798,9 @@ D'autres améliorations sont à venir ! Rôle initial No comment provided by engineer. - - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - Installer [SimpleX Chat pour terminal](https://github.com/simplex-chat/simplex-chat) + + Install SimpleX Chat for terminal + Installer SimpleX Chat pour terminal No comment provided by engineer. @@ -4534,7 +4853,7 @@ D'autres améliorations sont à venir ! Invalid connection link Lien de connection invalide - No comment provided by engineer. + conn error description Invalid display name! @@ -4554,7 +4873,15 @@ D'autres améliorations sont à venir ! Invalid name! Nom invalide ! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4590,6 +4917,10 @@ D'autres améliorations sont à venir ! Inviter des membres No comment provided by engineer. + + Invite someone privately + No comment provided by engineer. + Invite to chat Inviter à discuter @@ -4666,6 +4997,10 @@ D'autres améliorations sont à venir ! rejoindre entant que %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Rejoindre le groupe @@ -4752,6 +5087,14 @@ Voici votre lien pour le groupe %@ ! Quitter swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Quitter la discussion @@ -4776,6 +5119,10 @@ Voici votre lien pour le groupe %@ ! Less traffic on mobile networks. No comment provided by engineer. + + Let someone connect to you + No comment provided by engineer. + Let's talk in SimpleX Chat Discutons sur SimpleX Chat @@ -4796,6 +5143,10 @@ Voici votre lien pour le groupe %@ ! Liez vos applications mobiles et de bureau ! 🔗 No comment provided by engineer. + + Link signature verified. + owner verification + Linked desktop options Options de bureau lié @@ -4970,6 +5321,10 @@ Voici votre lien pour le groupe %@ ! Les membres du groupe peuvent ajouter des réactions aux messages. No comment provided by engineer. + + Members can chat with admins. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés. (24 heures) @@ -5033,6 +5388,10 @@ Voici votre lien pour le groupe %@ ! Brouillon de message No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Message transféré @@ -5126,6 +5485,14 @@ Voici votre lien pour le groupe %@ ! Les messages de %@ seront affichés ! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + E2EE info chat item + Messages in this chat will never be deleted. alert message @@ -5155,16 +5522,15 @@ Voici votre lien pour le groupe %@ ! Les messages, fichiers et appels sont protégés par un chiffrement **e2e résistant post-quantique** avec une confidentialité persistante, une répudiation et une récupération en cas d'effraction. No comment provided by engineer. + + Migrate + No comment provided by engineer. + Migrate device Transférer l'appareil No comment provided by engineer. - - Migrate from another device - Transférer depuis un autre appareil - No comment provided by engineer. - Migrate here Transférer ici @@ -5283,6 +5649,10 @@ Voici votre lien pour le groupe %@ ! Réseau et serveurs No comment provided by engineer. + + Network commitments + No comment provided by engineer. + Network connection Connexion au réseau @@ -5293,6 +5663,10 @@ Voici votre lien pour le groupe %@ ! Décentralisation du réseau No comment provided by engineer. + + Network error + conn error description + Network issues - message expired after many attempts to send it. Problèmes de réseau - le message a expiré après plusieurs tentatives d'envoi. @@ -5308,6 +5682,11 @@ Voici votre lien pour le groupe %@ ! Opérateur de réseau No comment provided by engineer. + + Network routers cannot know +who talks to whom + No comment provided by engineer. + Network settings Paramètres réseau @@ -5322,6 +5701,10 @@ Voici votre lien pour le groupe %@ ! New token status text + + New 1-time link + No comment provided by engineer. + New Passcode Nouveau code d'accès @@ -5347,6 +5730,10 @@ Voici votre lien pour le groupe %@ ! Nouvelle expérience de discussion 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Nouvelle demande de contact @@ -5415,11 +5802,28 @@ Voici votre lien pour le groupe %@ ! Non No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + No comment provided by engineer. + + + No active relays + No comment provided by engineer. + No app password Pas de mot de passe pour l'app Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats No comment provided by engineer. @@ -5557,11 +5961,22 @@ Voici votre lien pour le groupe %@ ! No unread chats No comment provided by engineer. - - No user identifiers. - Aucun identifiant d'utilisateur. + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. + + Non-profit governance + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + No comment provided by engineer. + + + Not all relays connected + alert title + Not compatible! Non compatible ! @@ -5616,7 +6031,7 @@ Voici votre lien pour le groupe %@ ! OK OK - No comment provided by engineer. + alert button Off @@ -5635,11 +6050,19 @@ new chat action Ancienne base de données No comment provided by engineer. + + On your phone, not on servers. + No comment provided by engineer. + One-time invitation link Lien d'invitation unique No comment provided by engineer. + + One-time link + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5659,6 +6082,10 @@ Nécessite l'activation d'un VPN. Les hôtes .onion ne seront pas utilisés. No comment provided by engineer. + + Only channel owners can change channel preferences. + No comment provided by engineer. + Only chat owners can change preferences. Seuls les propriétaires peuvent modifier les préférences. @@ -5758,7 +6185,8 @@ Nécessite l'activation d'un VPN. Open Ouvrir - alert action + alert action +alert button Open Settings @@ -5770,6 +6198,10 @@ Nécessite l'activation d'un VPN. Ouvrir les modifications No comment provided by engineer. + + Open channel + new chat action + Open chat Ouvrir le chat @@ -5789,6 +6221,10 @@ Nécessite l'activation d'un VPN. Ouvrir les conditions No comment provided by engineer. + + Open external link? + alert title + Open full link alert action @@ -5807,6 +6243,10 @@ Nécessite l'activation d'un VPN. Ouvrir le transfert vers un autre appareil authentication reason + + Open new channel + new chat action + Open new chat new chat action @@ -5846,6 +6286,13 @@ Nécessite l'activation d'un VPN. Serveur de l'opérateur alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + No comment provided by engineer. + Or import archive file Ou importer un fichier d'archive @@ -5866,6 +6313,10 @@ Nécessite l'activation d'un VPN. Ou partagez en toute sécurité le lien de ce fichier No comment provided by engineer. + + Or show QR in person or via video call. + No comment provided by engineer. + Or show this code Ou montrez ce code @@ -5876,6 +6327,10 @@ Nécessite l'activation d'un VPN. Ou à partager en privé No comment provided by engineer. + + Or use this QR - print or show online. + No comment provided by engineer. + Organize chats into lists No comment provided by engineer. @@ -5892,6 +6347,18 @@ Nécessite l'activation d'un VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + + + Ownership: you can run your own relays. + No comment provided by engineer. + PING count Nombre de PING @@ -5947,6 +6414,10 @@ Nécessite l'activation d'un VPN. Coller l'image No comment provided by engineer. + + Paste link / Scan + No comment provided by engineer. + Paste link to connect! Collez le lien pour vous connecter ! @@ -6097,6 +6568,14 @@ Erreur : %@ Conserver le brouillon du dernier message, avec les pièces jointes. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Adresse du serveur prédéfinie @@ -6131,13 +6610,12 @@ Erreur : %@ Privacy policy and conditions of use. No comment provided by engineer. - - Privacy redefined - La vie privée redéfinie + + Privacy: for owners and subscribers. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. + + Private and secure messaging. No comment provided by engineer. @@ -6178,6 +6656,10 @@ Erreur : %@ Private routing timeout alert title + + Proceed + alert action + Profile and server connections Profil et connexions au serveur @@ -6203,9 +6685,8 @@ Erreur : %@ Thème de profil No comment provided by engineer. - - Profile update will be sent to your contacts. - La mise à jour du profil sera envoyée à vos contacts. + + Profile update will be sent to your SimpleX contacts. alert message @@ -6213,6 +6694,10 @@ Erreur : %@ Interdire les appels audio/vidéo. No comment provided by engineer. + + Prohibit chats with admins. + No comment provided by engineer. + Prohibit irreversible message deletion. Interdire la suppression irréversible des messages. @@ -6242,6 +6727,10 @@ Erreur : %@ Interdire l'envoi de messages directs aux membres. No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + No comment provided by engineer. + Prohibit sending disappearing messages. Interdire l’envoi de messages éphémères. @@ -6308,6 +6797,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Le proxy est protégé par un mot de passe No comment provided by engineer. + + Public channels - speak freely 🚀 + No comment provided by engineer. + Push notifications Notifications push @@ -6348,24 +6841,14 @@ Activez-le dans les paramètres *Réseau et serveurs*. En savoir plus No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - Pour en savoir plus, consultez le [Guide de l'utilisateur](https ://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + Pour en savoir plus, consultez le Guide de l'utilisateur. No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - No comment provided by engineer. - - - Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - Pour en savoir plus, consultez le [Guide de l'utilisateur](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). - Pour en savoir plus, consultez notre [dépôt GitHub](https://github.com/simplex-chat/simplex-chat#readme). + + Read more in our GitHub repository. + Pour en savoir plus, consultez notre dépôt GitHub. No comment provided by engineer. @@ -6521,6 +7004,26 @@ swipe action Reject member? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + + + Relay results: + alert message + Relay server is only used if necessary. Another party can observe your IP address. Le serveur relais n'est utilisé que si nécessaire. Un tiers peut observer votre adresse IP. @@ -6531,6 +7034,14 @@ swipe action Le serveur relais protège votre adresse IP, mais il peut observer la durée de l'appel. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + + + Reliability: many relays per channel. + No comment provided by engineer. + Remove Supprimer @@ -6569,6 +7080,14 @@ swipe action Supprimer la phrase secrète de la keychain ? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. No comment provided by engineer. @@ -6789,6 +7308,10 @@ swipe action proxy SOCKS No comment provided by engineer. + + Safe web links + No comment provided by engineer. + Safely receive files Réception de fichiers en toute sécurité @@ -6814,6 +7337,10 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + alert button + Save admission settings? alert title @@ -6828,6 +7355,10 @@ chat item action Enregistrer et en informer les membres du groupe No comment provided by engineer. + + Save and notify subscribers + No comment provided by engineer. + Save and reconnect Sauvegarder et se reconnecter @@ -6838,6 +7369,14 @@ chat item action Enregistrer et mettre à jour le profil du groupe No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Enregistrer le profil du groupe @@ -7011,6 +7550,10 @@ chat item action Code de sécurité No comment provided by engineer. + + Security: owners hold channel keys. + No comment provided by engineer. + Select Choisir @@ -7137,6 +7680,10 @@ chat item action Send request without message No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. Envoyez-les depuis la phototèque ou des claviers personnalisés. @@ -7147,6 +7694,10 @@ chat item action Envoi des 100 derniers messages aux nouveaux membres. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + No comment provided by engineer. + Send your private feedback to groups. No comment provided by engineer. @@ -7161,6 +7712,10 @@ chat item action L'expéditeur a peut-être supprimé la demande de connexion. No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. L'envoi d'accusés de réception sera activé pour tous les contacts dans tous les profils de chat visibles. @@ -7286,6 +7841,10 @@ chat item action Le protocole du serveur a été modifié. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Le serveur requiert une autorisation pour créer des files d'attente, vérifiez le mot de passe @@ -7412,6 +7971,14 @@ chat item action Les paramètres ont été modifiés. alert message + + Setup notifications + No comment provided by engineer. + + + Setup routers + No comment provided by engineer. + Shape profile images Images de profil modelable @@ -7448,11 +8015,14 @@ chat item action Partager publiquement votre adresse No comment provided by engineer. - - Share address with contacts? - Partager l'adresse avec vos contacts ? + + Share address with SimpleX contacts? alert title + + Share channel + No comment provided by engineer. + Share from other apps. Partager depuis d'autres applications. @@ -7476,6 +8046,10 @@ chat item action Partager le profil No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Partagez ce lien d'invitation unique @@ -7486,9 +8060,12 @@ chat item action Partager sur SimpleX No comment provided by engineer. - - Share with contacts - Partager avec vos contacts + + Share via chat + No comment provided by engineer. + + + Share with SimpleX contacts No comment provided by engineer. @@ -7656,8 +8233,8 @@ chat item action Protocoles SimpleX audité par Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7732,6 +8309,11 @@ report reason Carré, circulaire, ou toute autre forme intermédiaire. No comment provided by engineer. + + Star on GitHub + Star sur GitHub + No comment provided by engineer. + Start chat Démarrer le chat @@ -7831,6 +8413,63 @@ report reason Inscriptions No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber reports + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers can add message reactions. + No comment provided by engineer. + + + Subscribers can chat with admins. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + No comment provided by engineer. + + + Subscribers can send direct messages. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + No comment provided by engineer. + + + Subscribers can send files and media. + No comment provided by engineer. + + + Subscribers can send voice messages. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Erreurs d'inscription @@ -7909,6 +8548,10 @@ report reason Prendre une photo No comment provided by engineer. + + Talk to someone + No comment provided by engineer. + Tap Connect to chat No comment provided by engineer. @@ -7921,9 +8564,8 @@ report reason Tap Connect to use bot No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. - Appuyez sur Créer une adresse SimpleX dans le menu pour la créer ultérieurement. + + Tap Join channel No comment provided by engineer. @@ -7955,6 +8597,10 @@ report reason Appuyez pour rejoindre incognito No comment provided by engineer. + + Tap to open + No comment provided by engineer. + Tap to paste link Appuyez pour coller le lien @@ -7973,12 +8619,17 @@ report reason Test failed at step %@. Échec du test à l'étape %@. - server test failure + relay test failure +server test failure Test notifications No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Tester le serveur @@ -8030,6 +8681,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. L'application protège votre vie privée en utilisant des opérateurs différents pour chaque conversation. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). L'application demandera de confirmer les téléchargements à partir de serveurs de fichiers inconnus (sauf .onion). @@ -8045,6 +8700,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Le code scanné n'est pas un code QR de lien SimpleX. No comment provided by engineer. + + The connection reached the limit of undelivered messages + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. La connexion a atteint la limite des messages non délivrés, votre contact est peut-être hors ligne. @@ -8070,9 +8729,9 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Le chiffrement fonctionne et le nouvel accord de chiffrement n'est pas nécessaire. Cela peut provoquer des erreurs de connexion ! No comment provided by engineer. - - The future of messaging - La nouvelle génération de messagerie privée + + The first network where you own +your contacts and groups. No comment provided by engineer. @@ -8109,6 +8768,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. L'ancienne base de données n'a pas été supprimée lors de la migration, elle peut être supprimée. No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + No comment provided by engineer. + The same conditions will apply to operator **%@**. Les mêmes conditions s'appliquent à l'opérateur **%@**. @@ -8154,6 +8817,14 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Thèmes No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + No comment provided by engineer. + These conditions will also apply for: **%@**. Ces conditions s'appliquent également aux : **%@**. @@ -8218,6 +8889,14 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Ce groupe n'existe plus. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -8264,6 +8943,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Pour cacher les messages indésirables. No comment provided by engineer. + + To make SimpleX Network last. + No comment provided by engineer. + To make a new connection Pour établir une nouvelle connexion @@ -8349,11 +9032,6 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s 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. - - Toggle chat list: - Afficher la liste des conversations : - No comment provided by engineer. - Toggle incognito when connecting. Basculer en mode incognito lors de la connexion. @@ -8368,6 +9046,10 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Opacité de la barre d'outils No comment provided by engineer. + + Top bar + No comment provided by engineer. + Total Total @@ -8432,6 +9114,10 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Débloquer ce membre ? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Messages non distribués @@ -8531,13 +9217,17 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Unsupported connection link - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. Les 100 derniers messages sont envoyés aux nouveaux membres. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + No comment provided by engineer. + Update Mise à jour @@ -8654,11 +9344,6 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Use TCP port 443 for preset servers only. No comment provided by engineer. - - Use chat - Utiliser le chat - No comment provided by engineer. - Use current profile Utiliser le profil actuel @@ -8674,6 +9359,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Utiliser pour les messages No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Utiliser pour les nouvelles connexions @@ -8713,6 +9402,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Utiliser le routage privé avec des serveurs inconnus. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Utiliser ce serveur @@ -8733,6 +9426,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Utiliser l'application d'une main. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + No comment provided by engineer. + Use web port No comment provided by engineer. @@ -8752,6 +9449,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Vous utilisez les serveurs SimpleX. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Vérifier le code avec le bureau @@ -8871,6 +9572,18 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Message vocal… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + + + Waiting for channel owner to add relays. + No comment provided by engineer. + Waiting for desktop... En attente du bureau... @@ -8911,6 +9624,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Attention : vous risquez de perdre des données ! No comment provided by engineer. + + We made connecting simpler for new users. + No comment provided by engineer. + WebRTC ICE servers Serveurs WebRTC ICE @@ -8960,6 +9677,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Lorsque vous partagez un profil incognito avec quelqu'un, ce profil sera utilisé pour les groupes auxquels il vous invite. No comment provided by engineer. + + Why SimpleX is built. + No comment provided by engineer. + WiFi WiFi @@ -9170,6 +9891,10 @@ Répéter la demande d'adhésion ? Vous pouvez configurer l'aperçu des notifications sur l'écran de verrouillage via les paramètres. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + 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. Vous pouvez partager un lien ou un code QR - n'importe qui pourra rejoindre le groupe. Vous ne perdrez pas les membres du groupe si vous le supprimez par la suite. @@ -9214,16 +9939,21 @@ Répéter la demande d'adhésion ? Vous ne pouvez pas envoyer de messages ! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Vous n'avez pas pu être vérifié·e ; veuillez réessayer. No comment provided by engineer. - - You decide who can connect. - Vous choisissez qui peut se connecter. - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -9290,6 +10020,10 @@ Répéter la demande de connexion ? You should receive notifications. token info + + You were born without an account + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. No comment provided by engineer. @@ -9324,6 +10058,10 @@ Répéter la demande de connexion ? Vous continuerez à recevoir des appels et des notifications des profils mis en sourdine lorsqu'ils sont actifs. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Vous ne recevrez plus de messages de cette discussion. L'historique sera préservé. @@ -9368,6 +10106,10 @@ Répéter la demande de connexion ? Vos appels No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Votre base de données de chat @@ -9416,6 +10158,10 @@ Répéter la demande de connexion ? Vos contacts resteront connectés. No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + No comment provided by engineer. + Your credentials may be sent unencrypted. Vos informations d'identification peuvent être envoyées non chiffrées. @@ -9435,6 +10181,10 @@ Répéter la demande de connexion ? Your group No comment provided by engineer. + + Your network + No comment provided by engineer. + Your preferences Vos préférences @@ -9450,6 +10200,11 @@ Répéter la demande de connexion ? Votre profil No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Votre profil **%@** sera partagé. @@ -9470,11 +10225,23 @@ Répéter la demande de connexion ? Votre profil a été modifié. Si vous l'enregistrez, le profil mis à jour sera envoyé à tous vos contacts. alert message + + Your public address + No comment provided by engineer. + Your random profile Votre profil aléatoire No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Votre adresse de serveur @@ -9490,21 +10257,11 @@ Répéter la demande de connexion ? Vos paramètres No comment provided by engineer. - - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - [Contribuer](https://github.com/simplex-chat/simplex-chat#contribute) - No comment provided by engineer. - [Send us email](mailto:chat@simplex.chat) [Contact par mail](mailto:chat@simplex.chat) No comment provided by engineer. - - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - [Star sur GitHub](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - \_italic_ \_italique_ @@ -9520,6 +10277,10 @@ Répéter la demande de connexion ? ci-dessus, puis choisissez : No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ rcv group event chat item @@ -9538,6 +10299,10 @@ Répéter la demande de connexion ? accepted you rcv group event chat item + + active + No comment provided by engineer. + admin admin @@ -9647,6 +10412,10 @@ marked deleted chat item preview text appel… call status + + can't broadcast + No comment provided by engineer. + can't send messages No comment provided by engineer. @@ -9681,6 +10450,14 @@ marked deleted chat item preview text changement d'adresse… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored coloré @@ -9823,6 +10600,10 @@ pref value supprimé deleted chat item + + deleted channel + rcv group event chat item + deleted contact contact supprimé @@ -9933,6 +10714,10 @@ pref value erreur No comment provided by engineer. + + error: %@ + receive error chat item + expired expiré @@ -10060,6 +10845,10 @@ pref value a quitté rcv group event chat item + + link + No comment provided by engineer. + marked deleted supprimé @@ -10128,6 +10917,10 @@ pref value jamais delete after time + + new + No comment provided by engineer. + new message nouveau message @@ -10245,6 +11038,10 @@ time to disappear appel rejeté call status + + relay + member role + removed supprimé @@ -10255,6 +11052,14 @@ time to disappear a retiré %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + + + removed by operator + No comment provided by engineer. + removed contact address suppression de l'adresse de contact @@ -10402,6 +11207,10 @@ dernier message reçu : %2$@ non protégé No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile mise à jour du profil de groupe @@ -10422,6 +11231,10 @@ dernier message reçu : %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link via le lien d'adresse du contact @@ -10496,6 +11309,10 @@ dernier message reçu : %2$@ vous êtes observateur No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ vous avez bloqué %@ @@ -10556,6 +11373,10 @@ dernier message reçu : %2$@ \~barré~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + owner verification + diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 0ed5dc19ea..7723cabdcb 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -185,6 +185,24 @@ %d hónap time interval + + %d relays failed + %d átjátszóhoz nem sikerült kapcsolódni + channel relay bar +channel subscriber relay bar + + + %d relays not active + %d átjátszó inaktív + channel relay bar +channel subscriber relay bar + + + %d relays removed + %d átjátszó eltávolítva + channel relay bar +channel subscriber relay bar + %d sec %d mp @@ -200,11 +218,63 @@ %d üzenet kihagyva integrity error chat item + + %d subscriber + %d feliratkozó + channel subscriber count + + + %d subscribers + %d feliratkozó + channel subscriber count + %d weeks %d hét time interval + + %1$d/%2$d relays active + %1$d/%2$d átjátszó aktív + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + %1$d/%2$d átjátszó aktív, %3$d hiba + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + %1$d/%2$d átjátszó aktív, %3$d sikertelen + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + %1$d/%2$d átjátszó aktív, %3$d eltávolítva + channel relay bar + + + %1$d/%2$d relays connected + %1$d/%2$d átjátszó kapcsolódva + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + %1$d/%2$d átjátszó kapcsolódva, %3$d hiba + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + %1$d/%2$d átjátszó kapcsolódott, %3$d átjátszóhoz nem sikerült kapcsolódni + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + %1$d/%2$d átjátszó kapcsolódott, %3$d eltávolítva + channel subscriber relay bar + %lld %lld @@ -215,6 +285,11 @@ %lld %@ No comment provided by engineer. + + %lld channel events + %lld csatornaesemény + No comment provided by engineer. + %lld contact(s) selected %lld partner kiválasztva @@ -315,11 +390,21 @@ %u üzenet kihagyva. No comment provided by engineer. + + (from owner) + (a tulajdonostól) + chat link info line + (new) (új) No comment provided by engineer. + + (signed) + (aláírva) + chat link info line + (this device v%@) (ez az eszköz: v%@) @@ -342,7 +427,7 @@ **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. - **Legprivátabb:** ne használja a SimpleX Chat értesítési kiszolgálót, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást). + **A legprivátabb**: Az alkalmazás nem használja a SimpleX Chat push-kiszolgálóját. Az alkalmazás a háttérben ellenőrzi az üzeneteket, amikor a rendszer ezt lehetővé teszi, attól függően, hogy Ön milyen gyakran használja az alkalmazást. No comment provided by engineer. @@ -352,7 +437,7 @@ **Please note**: you will NOT be able to recover or change passphrase if you lose it. - **Megjegyzés:** NEM fogja tudni helyreállítani, vagy módosítani a jelmondatot abban az esetben, ha elveszíti. + **Megjegyzés:** NEM fogja tudni helyreállítani vagy módosítani a jelmondatot abban az esetben, ha elveszíti. No comment provided by engineer. @@ -365,6 +450,11 @@ **Hivatkozás beolvasása / beillesztése**: egy kapott hivatkozáson keresztüli kapcsolódáshoz. No comment provided by engineer. + + **Test relay** to retrieve its name. + **Átjátszó tesztelése** a nevének lekéréséhez. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Figyelmeztetés:** Az azonnali leküldéses értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges. @@ -408,6 +498,15 @@ - és még sok más! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + - Hivatkozások előnézetének küldése. +- Hiperhivatkozásokon keresztüli adathalászat megakadályozása. +- Hivatkozások nyomonkövetési paramétereinek eltávolítása. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -476,7 +575,7 @@ time interval 1-time link can be used *with one contact only* - share in person or via any messenger. - Az egyszer használható meghívó egy hivatkozás és *csak egyetlen partnerrel használható* – személyesen vagy bármilyen üzenetváltó-alkalmazáson keresztül megosztható. + Az egyszer használható meghívó egy hivatkozás és *csak egyetlen partnerrel használható* – személyesen vagy bármilyen üzenetváltó alkalmazáson keresztül megosztható. No comment provided by engineer. @@ -506,6 +605,11 @@ time interval Néhány további dolog No comment provided by engineer. + + A link for one person to connect + Egy hivatkozás, ami egyetlen partnerrel való kapcsolat létrehozására szolgál + No comment provided by engineer. + A new contact Egy új partner @@ -632,9 +736,9 @@ swipe action Aktív kapcsolatok száma 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. - Cím hozzáadása a profilhoz, hogy a partnerei megoszthassák másokkal. A profilfrissítés el lesz küldve partnerei számára. + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. + Cím hozzáadása a profilhoz, hogy a SimpleX partnerei megoszthassák másokkal. A profilfrissítés el lesz küldve a SimpleX partnerei számára. No comment provided by engineer. @@ -702,6 +806,11 @@ swipe action Hozzáadott üzenetkiszolgálók No comment provided by engineer. + + Adding relays will be supported later. + Az átjátszók hozzáadása később lesz támogatott. + No comment provided by engineer. + Additional accent További kiemelőszín @@ -822,6 +931,16 @@ swipe action Összes profil profile dropdown + + All relays failed + Nem sikerült kapcsolódni egyetlen átjátszóhoz sem + No comment provided by engineer. + + + All relays removed + Az összes átjátszó el lett távolítva + No comment provided by engineer. + All reports will be archived for you. Az összes jelentés archiválva lesz az Ön számára. @@ -844,7 +963,7 @@ swipe action All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. - Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-továbbítókiszolgálókra. + Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-átjátszókra. No comment provided by engineer. @@ -882,6 +1001,11 @@ swipe action Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. (24 óra) No comment provided by engineer. + + Allow members to chat with admins. + A csevegés az adminisztrátorokkal engedélyezve van a tagok számára. + No comment provided by engineer. + Allow message reactions only if your contact allows them. A reakciók hozzáadása az üzenetekhez csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. @@ -897,6 +1021,11 @@ swipe action A közvetlen üzenetek küldése a tagok között engedélyezve van. No comment provided by engineer. + + Allow sending direct messages to subscribers. + A közvetlen üzenetek küldése a feliratkozók között engedélyezve van. + No comment provided by engineer. + Allow sending disappearing messages. Az eltűnő üzenetek küldése engedélyezve van. @@ -907,6 +1036,11 @@ swipe action Megosztás engedélyezése No comment provided by engineer. + + Allow subscribers to chat with admins. + A csevegés az adminisztrátorokkal engedélyezve van a feliratkozók számára. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Az elküldött üzenetek végleges törlése engedélyezve van. (24 óra) @@ -994,7 +1128,7 @@ swipe action Always use relay - Mindig legyen használva továbbítókiszolgáló + Mindig legyen használva átjátszó No comment provided by engineer. @@ -1012,11 +1146,6 @@ swipe action Hívás fogadása No comment provided by engineer. - - Anybody can host servers. - Bárki üzemeltethet kiszolgálókat. - No comment provided by engineer. - App build: %@ Alkalmazás összeállítási száma: %@ @@ -1222,6 +1351,23 @@ swipe action Hibás az üzenet kivonata No comment provided by engineer. + + Be free +in your network + Váljon szabaddá +a saját hálózatában + No comment provided by engineer. + + + Be free in your network. + Legyen szabad a saját hálózatában. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + Mert felszámoltuk a lehetőségét is annak, hogy megtudjuk, Ön kicsoda. Így az önrendelkezése soha nem kerülhet idegen kezekbe. + No comment provided by engineer. + Better calls Továbbfejlesztett hívásélmény @@ -1317,6 +1463,11 @@ swipe action Letiltja a tagot? No comment provided by engineer. + + Block subscriber for all? + Az összes feliratkozó számára letiltja a feliratkozót? + No comment provided by engineer. + Blocked by admin Letiltva az adminisztrátor által @@ -1367,6 +1518,16 @@ swipe action Mindkét fél küldhet hangüzeneteket. No comment provided by engineer. + + Bottom bar + Alsó sáv + No comment provided by engineer. + + + Broadcast + Közvetítés… + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Bolgár, finn, thai és ukrán – köszönet a felhasználóknak és a [Weblate-nek](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1375,7 +1536,7 @@ swipe action Business address Üzleti cím - No comment provided by engineer. + chat link info line Business chats @@ -1397,15 +1558,6 @@ swipe action A csevegési profillal (alapértelmezett), vagy a [kapcsolattal] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BÉTA). No comment provided by engineer. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - A SimpleX Chat használatával Ön elfogadja, hogy: -- csak elfogadott tartalmakat tesz közzé a nyilvános csoportokban. -- tiszteletben tartja a többi felhasználót, és nem küld kéretlen tartalmat senkinek. - No comment provided by engineer. - Call already ended! A hívás már véget ért! @@ -1554,6 +1706,82 @@ new chat action authentication reason set passcode view + + Channel + Csatorna + No comment provided by engineer. + + + Channel display name + Csatorna megjelenítendő neve + No comment provided by engineer. + + + Channel full name (optional) + Csatorna teljes neve (nem kötelező) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + A csatornának nincsenek aktív átjátszói. Próbáljon meg később csatlakozni. + alert message +alert subtitle + + + Channel image + Csatornakép + No comment provided by engineer. + + + Channel link + Csatornahivatkozás + chat link info line + + + Channel preferences + Csatornabeállítások + No comment provided by engineer. + + + Channel profile + Csatornaprofil + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + A csatornaprofil a feliratkozók eszközén és a csevegési átjátszókon van tárolva. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + A csatornaprofil módosult. Ha menti, akkor a frissített profil el lesz küldve a csatorna feliratkozóinak. + alert message + + + Channel temporarily unavailable + A csatorna ideiglenesen nem érhető el + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + A csatorna az összes feliratkozó számára törölve lesz – ez a művelet nem vonható vissza! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + A csatorna törölve lesz az Ön számára – ez a művelet nem vonható vissza! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + A csatorna %2$d átjátszóból %1$d használatával kezd el működni. Folytatja? + alert message + + + Channels + Csatornák + No comment provided by engineer. + Chat Csevegés @@ -1639,6 +1867,26 @@ set passcode view Csevegési profil No comment provided by engineer. + + Chat relay + Csevegési átjátszó + No comment provided by engineer. + + + Chat relays + Csevegési átjátszók + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + A csevegési átjátszók továbbítják az üzeneteket az Ön által létrehozott csatornákban. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + A csevegési átjátszók továbbítják az üzeneteket a csatorna feliratkozóinak. + No comment provided by engineer. + Chat theme Csevegés témája @@ -1657,7 +1905,8 @@ set passcode view Chat with admins Csevegés az adminisztrátorokkal - chat toolbar + chat feature +chat toolbar Chat with member @@ -1674,11 +1923,26 @@ set passcode view Csevegések No comment provided by engineer. + + Chats with admins are prohibited. + A csevegés az adminisztrátorokkal le van tiltva. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + A nyilvános csatornákban az adminisztrátorokkal való csevegések nem rendelkeznek végpontok közötti titkosítással – csak megbízható csevegési átjátszókkal használja őket. + alert message + Chats with members Csevegés a tagokkal No comment provided by engineer. + + Chats with members are disabled + A csevegés a tagokkal le van tiltva + No comment provided by engineer. + Check messages every 20 min. Üzenetek ellenőrzése 20 percenként. @@ -1689,6 +1953,16 @@ set passcode view Üzenetek ellenőrzése, amikor engedélyezett. No comment provided by engineer. + + Check relay address and try again. + Ellenőrizze az átjátszó címét, és próbálja újra. + alert message + + + Check relay name and try again. + Ellenőrizze az átjátszó nevét, és próbálja újra. + alert message + Check server address and try again. Kiszolgáló címének ellenőrzése és újrapróbálkozás. @@ -1834,9 +2108,9 @@ set passcode view ICE-kiszolgálók beállítása No comment provided by engineer. - - Configure server operators - Kiszolgálóüzemeltetők beállítása + + Configure relays + Átjátszók konfigurálása No comment provided by engineer. @@ -1897,7 +2171,8 @@ set passcode view Connect Kapcsolódás - server test step + relay test step +server test step Connect automatically @@ -1943,6 +2218,11 @@ Ez a saját egyszer használható meghívója! Kapcsolódás egy hivatkozáson keresztül new chat sheet title + + Connect via link or QR code + Hivatkozás vagy QR-kód használata + No comment provided by engineer. + Connect via one-time link Kapcsolódás az egyszer használható meghívón keresztül @@ -2021,10 +2301,11 @@ Ez a saját egyszer használható meghívója! Connection error (AUTH) Kapcsolódási hiba (AUTH) - No comment provided by engineer. + conn error description Connection failed + Nem sikerült létrehozni a kapcsolatot No comment provided by engineer. @@ -2079,6 +2360,11 @@ Ez a saját egyszer használható meghívója! Kapcsolatok No comment provided by engineer. + + Contact address + Kapcsolattartási cím + chat link info line + Contact allows Partner engedélyezi @@ -2149,6 +2435,11 @@ Ez a saját egyszer használható meghívója! Folytatás No comment provided by engineer. + + Contribute + Közreműködés + No comment provided by engineer. + Conversation deleted! Beszélgetés törölve! @@ -2161,7 +2452,7 @@ Ez a saját egyszer használható meghívója! Copy error - Másolási hiba + Hiba másolása No comment provided by engineer. @@ -2177,12 +2468,7 @@ Ez a saját egyszer használható meghívója! Correct name to %@? Helyesbíti a nevet a következőre: %@? - No comment provided by engineer. - - - Create - Létrehozás - No comment provided by engineer. + alert message Create 1-time link @@ -2234,6 +2520,16 @@ Ez a saját egyszer használható meghívója! Profil létrehozása No comment provided by engineer. + + Create public channel + Nyilvános csatorna létrehozása + No comment provided by engineer. + + + Create public channel (BETA) + Nyilvános csatorna létrehozása (BÉTA) + No comment provided by engineer. + Create queue Várólista létrehozása @@ -2244,11 +2540,21 @@ Ez a saját egyszer használható meghívója! Saját cím létrehozása No comment provided by engineer. + + Create your link + Saját hivatkozás létrehozása + No comment provided by engineer. + Create your profile Profil létrehozása No comment provided by engineer. + + Create your public address + Saját nyilvános cím létrehozása + No comment provided by engineer. + Created Létrehozva @@ -2269,6 +2575,11 @@ Ez a saját egyszer használható meghívója! Archívum hivatkozás létrehozása No comment provided by engineer. + + Creating channel + Csatorna létrehozása + No comment provided by engineer. + Creating link… Hivatkozás létrehozása… @@ -2427,10 +2738,10 @@ Ez a saját egyszer használható meghívója! Kézbesítési hibák felderítése No comment provided by engineer. - - Decentralized - Decentralizált - No comment provided by engineer. + + Decode link + Hivatkozás dekódolása + relay test step Decryption error @@ -2478,6 +2789,16 @@ swipe action Törlés, és a partner értesítése No comment provided by engineer. + + Delete channel + Csatorna törlése + No comment provided by engineer. + + + Delete channel? + Törli a csatornát? + No comment provided by engineer. + Delete chat Csevegés törlése @@ -2649,6 +2970,11 @@ alert button Várólista törlése server test step + + Delete relay + Átjátszó törlése + No comment provided by engineer. + Delete report Jelentés törlése @@ -2741,7 +3067,7 @@ alert button Destination server address of %@ is incompatible with forwarding server %@ settings. - A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbítókiszolgáló beállításaival. + A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbító kiszolgáló beállításaival. No comment provided by engineer. @@ -2751,7 +3077,7 @@ alert button Destination server version of %@ is incompatible with forwarding server %@. - A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbítókiszolgálóval. + A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbító kiszolgálóval. No comment provided by engineer. @@ -2814,6 +3140,16 @@ alert button A tagok közötti közvetlen üzenetek le vannak tiltva. No comment provided by engineer. + + Direct messages between subscribers are prohibited. + A feliratkozók közötti közvetlen üzenetek le vannak tiltva. + No comment provided by engineer. + + + Disable + Letiltás + alert button + Disable (keep overrides) Letiltás (egyéni beállítások megtartása) @@ -2919,6 +3255,11 @@ alert button Az előzmények ne legyenek elküldve az új tagok számára. No comment provided by engineer. + + Do not send history to new subscribers. + Az előzmények ne legyenek elküldve az új feliratkozók számára. + No comment provided by engineer. + Do not use credentials with proxy. Ne használja a hitelesítési adatokat proxyval. @@ -3020,11 +3361,21 @@ chat item action Végpontok között titkosított értesítések. No comment provided by engineer. + + Easier to invite your friends 👋 + Könnyebben hívhatja meg a barátait 👋 + No comment provided by engineer. + Edit Szerkesztés chat item action + + Edit channel profile + Csatornaprofil szerkesztése + No comment provided by engineer. + Edit group profile Csoportprofil szerkesztése @@ -3038,7 +3389,7 @@ chat item action Enable Engedélyezés - No comment provided by engineer. + alert button Enable (keep overrides) @@ -3060,6 +3411,11 @@ chat item action TCP életben tartása No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + Engedélyezzen legalább egy csevegési átjátszót a „Hálózat és kiszolgálók” menüben. + channel creation warning + Enable automatic message deletion? Engedélyezi az automatikus üzenettörlést? @@ -3070,6 +3426,11 @@ chat item action Kamera-hozzáférés engedélyezése No comment provided by engineer. + + Enable chats with admins? + Engedélyezi a csevegést az adminisztrátorokkal? + alert title + Enable disappearing messages by default. Eltűnő üzenetek engedélyezése alapértelmezetten. @@ -3090,16 +3451,16 @@ chat item action Engedélyezi az azonnali értesítéseket? No comment provided by engineer. + + Enable link previews? + Engedélyezi a hivatkozások előnézetét? + alert title + Enable lock Zárolás engedélyezése No comment provided by engineer. - - Enable notifications - Értesítések engedélyezése - No comment provided by engineer. - Enable periodic notifications? Engedélyezi az időszakos értesítéseket? @@ -3205,6 +3566,11 @@ chat item action Adja meg a jelkódot No comment provided by engineer. + + Enter channel name… + Adja meg a csatorna nevét… + No comment provided by engineer. + Enter correct passphrase. Adja meg a helyes jelmondatot. @@ -3230,6 +3596,16 @@ chat item action Adja meg a jelszót fentebb a megjelenítéshez! No comment provided by engineer. + + Enter profile name... + Profil nevének megadása… + No comment provided by engineer. + + + Enter relay name… + Adja meg az átjátszó nevét… + No comment provided by engineer. + Enter server manually Kiszolgáló megadása kézzel @@ -3258,7 +3634,7 @@ chat item action Error Hiba - No comment provided by engineer. + conn error description Error aborting address change @@ -3285,6 +3661,11 @@ chat item action Hiba történt a tag(ok) hozzáadásakor No comment provided by engineer. + + Error adding relay + Hiba az átjátszó hozzáadásakor + alert title + Error adding server Hiba történt a kiszolgáló hozzáadásakor @@ -3332,7 +3713,7 @@ chat item action Error connecting to forwarding server %@. Please try later. - Hiba történt a(z) %@ továbbítókiszolgálóhoz való kapcsolódáskor. Próbálja meg később. + Hiba történt a(z) %@ továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később. alert message @@ -3345,6 +3726,11 @@ chat item action Hiba történt a cím létrehozásakor No comment provided by engineer. + + Error creating channel + Hiba a csatorna létrehozásakor + alert title + Error creating group Hiba történt a csoport létrehozásakor @@ -3480,11 +3866,6 @@ chat item action Hiba történt a csevegés megnyitásakor No comment provided by engineer. - - Error opening group - Hiba történt a csoport megnyitásakor - No comment provided by engineer. - Error receiving file Hiba történt a fájl fogadásakor @@ -3530,6 +3911,11 @@ chat item action Hiba történt az ICE-kiszolgálók mentésekor No comment provided by engineer. + + Error saving channel profile + Hiba a csatornaprofil mentésekor + No comment provided by engineer. + Error saving chat list Hiba történt a csevegési lista mentésekor @@ -3595,6 +3981,11 @@ chat item action Hiba történt a kézbesítési jelentések beállításakor! No comment provided by engineer. + + Error sharing channel + Hiba a csatorna megosztásakor + alert title + Error starting chat Hiba történt a csevegés elindításakor @@ -3675,7 +4066,8 @@ snd error text Error: %@. Hiba: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3913,13 +4305,14 @@ snd error text Fingerprint in forwarding server address does not match certificate: %@. - A továbbítókiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@. + A továbbító kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@. No comment provided by engineer. Fingerprint in server address does not match certificate. A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3961,10 +4354,16 @@ snd error text Az összes moderátor számára No comment provided by engineer. + + For anyone to reach you + Bárki számára, aki el szeretné érni Önt + No comment provided by engineer. + For chat profile %@: A(z) %@ nevű csevegési profilhoz: - servers error + servers error +servers warning For console @@ -4038,30 +4437,30 @@ snd error text Forwarding server %1$@ failed to connect to destination server %2$@. Please try later. - A(z) %1$@ továbbítókiszolgáló nem tudott kapcsolódni a(z) %2$@ célkiszolgálóhoz. Próbálja meg később. + A(z) %1$@ továbbító kiszolgáló nem tudott kapcsolódni a(z) %2$@ célkiszolgálóhoz. Próbálja meg később. alert message Forwarding server address is incompatible with network settings: %@. - A továbbítókiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@. + A továbbító kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@. No comment provided by engineer. Forwarding server version is incompatible with network settings: %@. - A továbbítókiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@. + A továbbító kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@. No comment provided by engineer. Forwarding server: %1$@ Destination server error: %2$@ - Továbbítókiszolgáló: %1$@ + Továbbító kiszolgáló: %1$@ Célkiszolgáló-hiba: %2$@ snd error text Forwarding server: %1$@ Error: %2$@ - Továbbítókiszolgáló: %1$@ + Továbbító kiszolgáló: %1$@ Hiba: %2$@ snd error text @@ -4105,11 +4504,21 @@ Hiba: %2$@ GIF-ek és matricák No comment provided by engineer. + + Get link + Hivatkozás megtekintése + relay test step + Get notified when mentioned. Kapjon értesítést, ha megemlítik. No comment provided by engineer. + + Get started + Vágjunk bele + No comment provided by engineer. + Good afternoon! Jó napot! @@ -4168,7 +4577,7 @@ Hiba: %2$@ Group link Csoporthivatkozás - No comment provided by engineer. + chat link info line Group links @@ -4280,6 +4689,11 @@ Hiba: %2$@ Az előzmények nem lesznek elküldve az új tagok számára. No comment provided by engineer. + + History is not sent to new subscribers. + Az előzmények nem lesznek elküldve az új feliratkozók számára. + No comment provided by engineer. + How SimpleX works Hogyan működik a SimpleX @@ -4347,6 +4761,7 @@ Hiba: %2$@ If you joined or created channels, they will stop working permanently. + Ha csatornákat hozott létre vagy csak csatlakozott hozzájuk, akkor azok véglegesen le fognak állni. down migration warning @@ -4379,11 +4794,6 @@ Hiba: %2$@ Azonnal No comment provided by engineer. - - Immune to spam - Védett a kéretlen tartalommal szemben - No comment provided by engineer. - Import Importálás @@ -4526,9 +4936,9 @@ További fejlesztések hamarosan! Kezdeti szerepkör No comment provided by engineer. - - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - A [SimpleX Chat terminálhoz] telepítése (https://github.com/simplex-chat/simplex-chat) + + Install SimpleX Chat for terminal + A SimpleX Chat terminálhoz telepítése No comment provided by engineer. @@ -4586,7 +4996,7 @@ További fejlesztések hamarosan! Invalid connection link Érvénytelen kapcsolattartási hivatkozás - No comment provided by engineer. + conn error description Invalid display name! @@ -4606,7 +5016,17 @@ További fejlesztések hamarosan! Invalid name! Érvénytelen név! - No comment provided by engineer. + alert title + + + Invalid relay address! + Érvénytelen az átjátszó címe! + alert title + + + Invalid relay name! + Érvénytelen az átjátszó neve! + alert title Invalid response @@ -4643,6 +5063,11 @@ További fejlesztések hamarosan! Tagok meghívása No comment provided by engineer. + + Invite someone privately + Partner meghívása privátban + No comment provided by engineer. + Invite to chat Meghívás a csevegésbe @@ -4719,6 +5144,11 @@ További fejlesztések hamarosan! Csatlakozás mint: %@ No comment provided by engineer. + + Join channel + Csatlakozás a csatornához + No comment provided by engineer. + Join group Csatlakozás a csoporthoz @@ -4806,6 +5236,16 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Elhagyás swipe action + + Leave channel + Csatorna elhagyása + No comment provided by engineer. + + + Leave channel? + Elhagyja a csatornát? + No comment provided by engineer. + Leave chat Csevegés elhagyása @@ -4831,6 +5271,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Kevesebb adatforgalom a mobilhálózatokon. No comment provided by engineer. + + Let someone connect to you + Hagyja, hogy valaki elérje Önt + No comment provided by engineer. + Let's talk in SimpleX Chat Beszélgessünk a SimpleX Chatben @@ -4851,6 +5296,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Társítsa össze a hordozható eszköz- és a számítógépes alkalmazásokat! 🔗 No comment provided by engineer. + + Link signature verified. + Hivatkozás aláírása ellenőrizve. + owner verification + Linked desktop options Társított számítógép beállítások @@ -5036,6 +5486,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! A tagok reakciókat adhatnak hozzá az üzenetekhez. No comment provided by engineer. + + Members can chat with admins. + A tagok cseveghetnek az adminisztrátorokkal + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) A tagok véglegesen törölhetik az elküldött üzeneteiket. (24 óra) @@ -5101,6 +5556,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Piszkozatok No comment provided by engineer. + + Message error + Üzenethiba + No comment provided by engineer. + Message forwarded Továbbított üzenet @@ -5196,6 +5656,16 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! %@ összes üzenete meg fog jelenni! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + Ebben a csatornában az üzenetek **nem rendelkeznek végpontok közötti titkosítással**. A csevegési átjátszók láthatják ezeket az üzeneteket. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + Ebben a csatornában az üzenetek nem rendelkeznek végpontok közötti titkosítással. A csevegési átjátszók láthatják ezeket az üzeneteket. + E2EE info chat item + Messages in this chat will never be deleted. Az ebben a csevegésben lévő üzenetek soha nem lesznek törölve. @@ -5213,7 +5683,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Messages were deleted after you selected them. - Az üzeneteket törölték miután kiváasztotta őket. + Az üzeneteket törölték miután kiválasztotta őket. alert message @@ -5226,16 +5696,16 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve. No comment provided by engineer. + + Migrate + Átköltöztetés + No comment provided by engineer. + Migrate device Eszköz átköltöztetése No comment provided by engineer. - - Migrate from another device - Átköltöztetés egy másik eszközről - No comment provided by engineer. - Migrate here Átköltöztetés ide @@ -5356,6 +5826,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Hálózat és kiszolgálók No comment provided by engineer. + + Network commitments + Hálózati kötelezettségvállalások + No comment provided by engineer. + Network connection Hálózati kapcsolat @@ -5366,6 +5841,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Hálózati decentralizáció No comment provided by engineer. + + Network error + Hálózati hiba + conn error description + Network issues - message expired after many attempts to send it. Hálózati problémák – az üzenet többszöri elküldési kísérlet után lejárt. @@ -5381,6 +5861,13 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Hálózatüzemeltető No comment provided by engineer. + + Network routers cannot know +who talks to whom + A hálózati útválasztók nem tudhatják, +hogy ki kivel beszélget + No comment provided by engineer. + Network settings Hálózati beállítások @@ -5396,6 +5883,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Új token status text + + New 1-time link + Új egyszer használható meghívó + No comment provided by engineer. + New Passcode Új jelkód @@ -5421,6 +5913,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Új csevegési élmény 🎉 No comment provided by engineer. + + New chat relay + Új csevegési átjátszó + No comment provided by engineer. + New contact request Új partneri kapcsolatkérés @@ -5491,11 +5988,33 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Nem No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + Nincs fiók. Nincs telefonszám. Nincs e-mail-cím. Nincs személyazonosító. +A legbiztonságosabb titkosítás. + No comment provided by engineer. + + + No active relays + Nincsenek aktív átjátszók + No comment provided by engineer. + No app password Nincs alkalmazás jelszó Authentication unavailable + + No chat relays + Nincsenek csevegési átjátszók + No comment provided by engineer. + + + No chat relays enabled. + Nincsenek engedélyezve csevegési átjátszók. + servers warning + No chats Nincsenek csevegések @@ -5641,11 +6160,26 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Nincsenek olvasatlan csevegések No comment provided by engineer. - - No user identifiers. - Nincsenek felhasználói azonosítók. + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. + Senki sem követte nyomon a beszélgetéseinket. Senki sem készített térképet arról, hogy merre jártunk. A magánéletünk nem csak egy funkció volt, hanem az életmódunk. No comment provided by engineer. + + Non-profit governance + Nonprofit irányítás + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + Nem egy jobb zár mások ajtaján. Nem egy kedvesebb házmester, aki tiszteletben tartja az Ön magánéletét, de mégis nyilvántartást vezet minden látogatójáról. Ön itt nem csak egy vendég. Ön itt otthon van. Nincs az a hatalom, amely beléphetne ide - Ön itt szuverén. + No comment provided by engineer. + + + Not all relays connected + Nem minden átjátszó kapcsolódott + alert title + Not compatible! Nem kompatibilis! @@ -5703,7 +6237,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! OK Rendben - No comment provided by engineer. + alert button Off @@ -5722,11 +6256,21 @@ new chat action Régi adatbázis No comment provided by engineer. + + On your phone, not on servers. + Az eszközön, nem pedig kiszolgálókon. + No comment provided by engineer. + One-time invitation link Egyszer használható meghívó No comment provided by engineer. + + One-time link + Egyszer használható meghívó + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5746,6 +6290,11 @@ VPN engedélyezése szükséges. Az onion kiszolgálók nem lesznek használva. No comment provided by engineer. + + Only channel owners can change channel preferences. + Csak a csatorna tulajdonosai módosíthatják a csatornabeállításokat. + No comment provided by engineer. + Only chat owners can change preferences. Csak a csevegés tulajdonosai módosíthatják a csevegési beállításokat. @@ -5849,7 +6398,8 @@ VPN engedélyezése szükséges. Open Megnyitás - alert action + alert action +alert button Open Settings @@ -5861,6 +6411,11 @@ VPN engedélyezése szükséges. Módosítások megtekintése No comment provided by engineer. + + Open channel + Csatorna megnyitása + new chat action + Open chat Csevegés megnyitása @@ -5881,6 +6436,11 @@ VPN engedélyezése szükséges. Feltételek megnyitása No comment provided by engineer. + + Open external link? + Megnyitja a külső hivatkozást? + alert title + Open full link Teljes hivatkozás megnyitása @@ -5901,6 +6461,11 @@ VPN engedélyezése szükséges. Átköltöztetés indítása egy másik eszközre authentication reason + + Open new channel + Új csatorna megnyitása + new chat action + Open new chat Új csevegés megnyitása @@ -5946,6 +6511,17 @@ VPN engedélyezése szükséges. Kiszolgáló-üzemeltető alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + Az üzemeltetők kijelentik, hogy: +- függetlenek maradnak +- minimálisra csökkentik a metaadatok használatát +- ellenőrzött, nyílt forráskódú szoftvereket futtatnak + No comment provided by engineer. + Or import archive file Vagy archívumfájl importálása @@ -5966,6 +6542,11 @@ VPN engedélyezése szükséges. Vagy ossza meg biztonságosan ezt a fájlhivatkozást No comment provided by engineer. + + Or show QR in person or via video call. + Vagy mutassa meg a QR-kódot személyesen vagy videóhíváson keresztül. + No comment provided by engineer. + Or show this code Vagy mutassa meg ezt a kódot @@ -5976,6 +6557,11 @@ VPN engedélyezése szükséges. Vagy a privát megosztáshoz No comment provided by engineer. + + Or use this QR - print or show online. + Vagy használja ezt a QR-kódot – nyomtassa ki vagy mutassa meg online. + No comment provided by engineer. + Organize chats into lists Csevegések listákba szervezése @@ -5993,6 +6579,21 @@ VPN engedélyezése szükséges. %@ alert message + + Owner + Tulajdonos + No comment provided by engineer. + + + Owners + Tulajdonosok + No comment provided by engineer. + + + Ownership: you can run your own relays. + Tulajdonjog: saját átjátszókat üzemeltethet. + No comment provided by engineer. + PING count PING-ek száma @@ -6048,6 +6649,11 @@ VPN engedélyezése szükséges. Kép beillesztése No comment provided by engineer. + + Paste link / Scan + Hivatkozás megadása vagy QR-kód beolvasása + No comment provided by engineer. + Paste link to connect! Hivatkozás beillesztése a kapcsolódáshoz! @@ -6202,9 +6808,19 @@ Hiba: %@ Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. No comment provided by engineer. + + Preset relay address + Előre beállított átjátszó címe + No comment provided by engineer. + + + Preset relay name + Előre beállított átjátszó neve + No comment provided by engineer. + Preset server address - Az előre beállított kiszolgáló címe + Előre beállított kiszolgáló címe No comment provided by engineer. @@ -6237,14 +6853,14 @@ Hiba: %@ Adatvédelmi szabályzat és felhasználási feltételek. No comment provided by engineer. - - Privacy redefined - Újraértelmezett adatvédelem + + Privacy: for owners and subscribers. + Adatvédelem: tulajdonosok és előfizetők számára. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. - A privát csevegések, a csoportok és a partnerek nem érhetők el a kiszolgálók üzemeltetői számára. + + Private and secure messaging. + Privát és biztonságos üzenetváltás. No comment provided by engineer. @@ -6287,6 +6903,11 @@ Hiba: %@ Privát útválasztás időtúllépése alert title + + Proceed + Folytatás + alert action + Profile and server connections Profil és kiszolgálókapcsolatok @@ -6312,9 +6933,9 @@ Hiba: %@ Profiltéma No comment provided by engineer. - - Profile update will be sent to your contacts. - A profilfrissítés el lesz küldve a partnerei számára. + + Profile update will be sent to your SimpleX contacts. + A profilfrissítés el lesz küldve a SimpleX partnerei számára. alert message @@ -6322,6 +6943,11 @@ Hiba: %@ A hívások kezdeményezése le van tiltva. No comment provided by engineer. + + Prohibit chats with admins. + A csevegés az adminisztrátorokkal le van tiltva. + No comment provided by engineer. + Prohibit irreversible message deletion. Az elküldött üzenetek végleges törlése le van tiltva. @@ -6352,6 +6978,11 @@ Hiba: %@ A közvetlen üzenetek küldése a tagok között le van tiltva. No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + A közvetlen üzenetek küldése a feliratkozók között le van tiltva. + No comment provided by engineer. + Prohibit sending disappearing messages. Az eltűnő üzenetek küldése le van tiltva. @@ -6380,7 +7011,7 @@ Hiba: %@ Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings. - Védje az IP-címét a partnerei által kiválasztott üzenetváltási továbbítókiszolgálókkal szemben. + Védje az IP-címét a partnerei által kiválasztott üzenetváltási átjátszókkal szemben. Engedélyezze a *Hálózat és kiszolgálók* menüben. No comment provided by engineer. @@ -6419,6 +7050,11 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. A proxy jelszót igényel No comment provided by engineer. + + Public channels - speak freely 🚀 + Nyilvános csatornák – mondja el szabadon a véleményét 🚀 + No comment provided by engineer. + Push notifications Leküldéses értesítések @@ -6459,24 +7095,14 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Tudjon meg többet No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + További információ a Használati útmutatóban. No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - No comment provided by engineer. - - - Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - További információ a [Használati útmutatóban](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). - További információ a [GitHub-tárolónkban](https://github.com/simplex-chat/simplex-chat#readme). + + Read more in our GitHub repository. + További információ a GitHub-tárolónkban. No comment provided by engineer. @@ -6636,14 +7262,49 @@ swipe action Elutasítja a tagot? alert title + + Relay + Átjátszó + No comment provided by engineer. + + + Relay address + Átjátszó címe + alert title + + + Relay connection failed + Nem sikerült kapcsolódni az átjátszóhoz + alert title + + + Relay link + Átjátszóhivatkozás + No comment provided by engineer. + + + Relay results: + Átjátszóeredmények: + alert message + Relay server is only used if necessary. Another party can observe your IP address. - A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét. + Az átjátszó csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét. No comment provided by engineer. Relay server protects your IP address, but it can observe the duration of the call. - A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát. + Az átjátszó megvédi az IP-címét, de megfigyelheti a hívás időtartamát. + No comment provided by engineer. + + + Relay test failed! + Nem sikerült tesztelni az átjátszót! + No comment provided by engineer. + + + Reliability: many relays per channel. + Megbízhatóság: több átjátszó is használható csatornánként. No comment provided by engineer. @@ -6686,6 +7347,16 @@ swipe action Eltávolítja a jelmondatot a kulcstartóból? No comment provided by engineer. + + Remove subscriber + Feliratkozó eltávolítása + No comment provided by engineer. + + + Remove subscriber? + Eltávolítja a feliratkozót? + alert title + Removes messages and blocks members. Üzenetek eltávolítása és a tagok tiltása. @@ -6921,6 +7592,11 @@ swipe action SOCKS proxy No comment provided by engineer. + + Safe web links + Biztonságos webhivatkozások + No comment provided by engineer. + Safely receive files Fájlok biztonságos fogadása @@ -6947,6 +7623,11 @@ chat item action Mentés (és a tagok értesítése) alert button + + Save (and notify subscribers) + Mentés (és a feliratkozók értesítése) + alert button + Save admission settings? Menti a befogadási beállításokat? @@ -6962,6 +7643,11 @@ chat item action Mentés és a csoporttagok értesítése No comment provided by engineer. + + Save and notify subscribers + Mentés és a feliratkozók értesítése + No comment provided by engineer. + Save and reconnect Mentés és újrakapcsolódás @@ -6972,6 +7658,16 @@ chat item action Mentés és a csoportprofil frissítése No comment provided by engineer. + + Save channel profile + Csatornaprofil mentése + No comment provided by engineer. + + + Save channel profile? + Menti a csatornaprofilt? + alert title + Save group profile Csoportprofil mentése @@ -7114,7 +7810,7 @@ chat item action Search or paste SimpleX link - Keresés vagy SimpleX-hivatkozás beillesztése + Keressen vagy adjon meg egy SimpleX-hivatkozást No comment provided by engineer. @@ -7152,6 +7848,11 @@ chat item action Biztonsági kód No comment provided by engineer. + + Security: owners hold channel keys. + Biztonság: a csatornák kulcsait a tulajdonosok őrzik. + No comment provided by engineer. + Select Kiválasztás @@ -7282,6 +7983,11 @@ chat item action Kérés küldése üzenet nélkül No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + Küldje el a hivatkozást bármilyen üzenetváltó alkalmazáson keresztül – ez egy biztonságos módszer – és kérje meg a partnerét, hogy illessze be a SimpleX alkalmazásba. + No comment provided by engineer. + Send them from gallery or custom keyboards. Küldje el őket a galériából vagy az egyéni billentyűzetekről. @@ -7292,6 +7998,11 @@ chat item action Legfeljebb az utolsó 100 üzenet elküldése az új tagok számára. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + Legfeljebb az utolsó 100 üzenet elküldése az új feliratkozók számára. + No comment provided by engineer. + Send your private feedback to groups. Küldjön privát visszajelzést a csoportoknak. @@ -7307,6 +8018,11 @@ chat item action A kérés küldője törölhette a kapcsolódási kérést. No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + A hivatkozáselőnézet küldése felfedheti az Ön IP-címét a weboldal számára. Ezt később módosíthatja az adatvédelmi beállításokban. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. A kézbesítési jelentések küldése engedélyezve lesz az összes látható csevegési profilban lévő összes partnere számára. @@ -7432,6 +8148,11 @@ chat item action A kiszolgálóprotokoll módosult. alert title + + Server requires authorization to connect to relay, check password. + A kiszolgáló hitelesítést igényel az átjátszóhoz való kapcsolódáshoz, ellenőrizze a jelszavát. + relay test error + Server requires authorization to create queues, check password. A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze a jelszavát. @@ -7562,6 +8283,16 @@ chat item action A beállítások módosultak. alert message + + Setup notifications + Értesítések beállítása + No comment provided by engineer. + + + Setup routers + Útválasztók beállítása + No comment provided by engineer. + Shape profile images Profilkép alakzata @@ -7598,11 +8329,16 @@ chat item action Cím nyilvános megosztása No comment provided by engineer. - - Share address with contacts? - Megosztja a címet a partnereivel? + + Share address with SimpleX contacts? + Megosztja a címet a SimpleX partnereivel? alert title + + Share channel + Csatorna megosztása + No comment provided by engineer. + Share from other apps. Megosztás más alkalmazásokból. @@ -7628,6 +8364,11 @@ chat item action Profil megosztása No comment provided by engineer. + + Share relay address + Átjátszó címének megosztása + No comment provided by engineer. + Share this 1-time invite link Ennek az egyszer használható meghívónak a megosztása @@ -7638,9 +8379,14 @@ chat item action Megosztás a SimpleXben No comment provided by engineer. - - Share with contacts - Megosztás a partnerekkel + + Share via chat + Megosztás egy csevegésen keresztül + No comment provided by engineer. + + + Share with SimpleX contacts + Megosztás a SimpleX partnerekkel No comment provided by engineer. @@ -7755,7 +8501,7 @@ chat item action SimpleX address and 1-time links are safe to share via any messenger. - A SimpleX-cím és az egyszer használható meghívó biztonságosan megosztható bármilyen üzenetváltó-alkalmazáson keresztül. + A SimpleX-cím és az egyszer használható meghívó biztonságosan megosztható bármilyen üzenetváltó alkalmazáson keresztül. No comment provided by engineer. @@ -7813,9 +8559,9 @@ chat item action A SimpleX protokollokat a Trail of Bits auditálta. No comment provided by engineer. - - SimpleX relay link - SimpleX továbbítókiszolgáló-hivatkozás + + SimpleX relay address + SimpleX-átjátszó címe simplex link type @@ -7891,6 +8637,11 @@ report reason Négyzet, kör vagy bármi a kettő között. No comment provided by engineer. + + Star on GitHub + Csillagozás a GitHubon + No comment provided by engineer. + Start chat Csevegés elindítása @@ -7991,6 +8742,78 @@ report reason Feliratkozva No comment provided by engineer. + + Subscriber + Feliratkozó + No comment provided by engineer. + + + Subscriber reports + Feliratkozók jelentései + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + A feliratkozó el lesz távolítva a csatornából – ez a művelet nem vonható vissza! + alert message + + + Subscribers + Feliratkozók + No comment provided by engineer. + + + Subscribers can add message reactions. + A feliratkozók reakciókat adhatnak hozzá az üzenetekhez. + No comment provided by engineer. + + + Subscribers can chat with admins. + A feliratkozók cseveghetnek az adminisztrátorokkal. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + A feliratkozók véglegesen törölhetik az elküldött üzeneteiket. (24 óra) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + A feliratkozók jelenthetik az üzeneteket a moderátorok felé. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + A feliratkozók küldhetnek SimpleX-hivatkozásokat. + No comment provided by engineer. + + + Subscribers can send direct messages. + A feliratkozók küldhetnek egymásnak közvetlen üzeneteket. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + A feliratkozók küldhetnek eltűnő üzeneteket. + No comment provided by engineer. + + + Subscribers can send files and media. + A feliratkozók küldhetnek fájlokat és médiatartalmakat. + No comment provided by engineer. + + + Subscribers can send voice messages. + A feliratkozók küldhetnek hangüzeneteket. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + A feliratkozók az átjátszó hivatkozását használják a csatornához való kapcsolódáshoz. +Az átjátszó címe ennek az átjátszónak a beállítására szolgált a csatornához. + No comment provided by engineer. + Subscription errors Feliratkozási hibák @@ -8071,6 +8894,11 @@ report reason Kép készítése No comment provided by engineer. + + Talk to someone + Beszélgessen valakivel + No comment provided by engineer. + Tap Connect to chat Koppintson a „Kapcsolódás” gombra a csevegéshez @@ -8086,9 +8914,9 @@ report reason Koppintson a „Kapcsolódás” gombra a bot használatához No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. - Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz. + + Tap Join channel + Koppintson a „Csatlakozás a csatornához” gombra No comment provided by engineer. @@ -8121,6 +8949,11 @@ report reason Koppintson ide az inkognitóban való kapcsolódáshoz No comment provided by engineer. + + Tap to open + Koppintson ide a megnyitáshoz + No comment provided by engineer. + Tap to paste link Koppintson ide a hivatkozás beillesztéséhez @@ -8139,13 +8972,19 @@ report reason Test failed at step %@. A teszt a(z) %@ lépésnél sikertelen volt. - server test failure + relay test failure +server test failure Test notifications Értesítések tesztelése No comment provided by engineer. + + Test relay + Átjátszó tesztelése + No comment provided by engineer. + Test server Kiszolgáló tesztelése @@ -8198,6 +9037,11 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetéshez más-más üzemeltetőt használ. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + Az alkalmazás %lld sikertelen letöltési kísérlet után eltávolította ezt az üzenetet. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról (kivéve .onion) történő letöltések megerősítését. @@ -8213,6 +9057,11 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. A beolvasott QR-kód nem egy SimpleX-hivatkozás. No comment provided by engineer. + + The connection reached the limit of undelivered messages + A kapcsolat elérte a kézbesítetlen üzenetek korlátját + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. A kapcsolat elérte a kézbesítetlen üzenetek számának határát, a partnere lehet, hogy offline állapotban van. @@ -8238,9 +9087,11 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet! No comment provided by engineer. - - The future of messaging - Az üzenetváltás jövője + + The first network where you own +your contacts and groups. + Az első hálózat, ahol Ön birtokolja +a saját kapcsolatait és csoportjait. No comment provided by engineer. @@ -8278,6 +9129,11 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. A régi adatbázis nem lett eltávolítva az átköltöztetéskor, ezért törölhető. No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + A legrégebbi emberi szabadság - beszélgetni az emberekkel, anélkül, hogy mások megfigyelnének - olyan infrastruktúrán alapul, amely nem tudja elárulni. + No comment provided by engineer. + The same conditions will apply to operator **%@**. Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető számára is: **%@**. @@ -8323,6 +9179,16 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. Témák No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + Aztán felléptünk az internetre, és minden platform kért belőlünk egy darabot - nevet, telefonszámot, baráti kapcsolatokat. Elfogadtuk, hogy a kommunikáció ára az, hogy mások megtudják, hogy kivel beszélünk. Minden generáció, az emberek és a technológia is eddig így működött - telefon, e-mail, üzenetküldő programok, közösségi média. Úgy tűnt, ez az egyetlen lehetséges mód. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + De van egy másik lehetőség is. Egy hálózat, amelyben nincsenek telefonszámok. Nincsenek felhasználónevek. Nincsenek fiókok. Nincsenek semmiféle felhasználói azonosítók. Egy hálózat, amely összeköti az embereket és titkosított üzeneteket továbbít, anélkül, hogy tudná, ki csatlakozik hozzá. + No comment provided by engineer. + These conditions will also apply for: **%@**. Ezek a feltételek lesznek elfogadva a következő számára is: **%@**. @@ -8388,6 +9254,16 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. Ez a csoport már nem létezik. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + Ez egy csevegési átjátszó címe, nem használható kapcsolódásra. + alert message + + + This is your link for channel %@! + Ez a saját hivatkozása a(z) %@ nevű csatornához! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Ez a hivatkozás újabb alkalmazásverziót igényel. Frissítse az alkalmazást vagy kérjen egy kompatibilis hivatkozást a partnerétől. @@ -8438,6 +9314,11 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. Kéretlen üzenetek elrejtése. No comment provided by engineer. + + To make SimpleX Network last. + A SimpleX hálózat hosszú távú működésének biztosítása érdekében. + No comment provided by engineer. + To make a new connection Új kapcsolat létrehozásához @@ -8525,11 +9406,6 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll A végpontok közötti titkosítás ellenőrzéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) a partnere eszközén lévő kóddal. No comment provided by engineer. - - Toggle chat list: - Csevegési lista ki/be: - No comment provided by engineer. - Toggle incognito when connecting. Inkognitó profil használata kapcsolódáskor ki/be. @@ -8545,6 +9421,11 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Eszköztár átlátszatlansága No comment provided by engineer. + + Top bar + Felső sáv + No comment provided by engineer. + Total Összes kapcsolat @@ -8610,6 +9491,11 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Feloldja a tag letiltását? No comment provided by engineer. + + Unblock subscriber for all? + Az összes feliratkozó számára feloldja a feliratkozó letiltását? + No comment provided by engineer. + Undelivered messages Kézbesítetlen üzenetek @@ -8710,13 +9596,18 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Unsupported connection link Nem támogatott kapcsolattartási hivatkozás - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. Legfeljebb az utolsó 100 üzenet lesz elküldve az új tagok számára. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + Legfeljebb az utolsó 100 üzenet lesz elküldve az új feliratkozók számára. + No comment provided by engineer. + Update Frissítés @@ -8842,11 +9733,6 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso A 443-as TCP-port használata kizárólag az előre beállított kiszolgálókhoz. No comment provided by engineer. - - Use chat - SimpleX Chat használata - No comment provided by engineer. - Use current profile Jelenlegi profil használata @@ -8862,6 +9748,11 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Használat az üzenetekhez No comment provided by engineer. + + Use for new channels + Használat új csatornákhoz + No comment provided by engineer. + Use for new connections Használat új kapcsolatokhoz @@ -8902,6 +9793,11 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Privát útválasztás használata az ismeretlen kiszolgálókhoz. No comment provided by engineer. + + Use relay + Átjátszó használata + No comment provided by engineer. + Use server Kiszolgáló használata @@ -8922,6 +9818,11 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Alkalmazás egy kézzel való használata. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + Használja ezt a címet a közösségi oldalakon használt profiljaiban, weboldalakon vagy az e-mail aláírásában. + No comment provided by engineer. + Use web port Webport használata @@ -8942,6 +9843,11 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso SimpleX Chat kiszolgálók használatban. No comment provided by engineer. + + Verify + Ellenőrzés + relay test step + Verify code with desktop Kód ellenőrzése a számítógépen @@ -9062,6 +9968,21 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Hangüzenet… No comment provided by engineer. + + Wait + Várakozás + alert action + + + Wait response + Várakozás a válaszra + relay test step + + + Waiting for channel owner to add relays. + Várakozás a csatorna tulajdonosára az átjátszók hozzáadásához. + No comment provided by engineer. + Waiting for desktop... Várakozás a számítógép-alkalmazásra… @@ -9102,6 +10023,11 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Figyelmeztetés: néhány adat elveszhet! No comment provided by engineer. + + We made connecting simpler for new users. + Az új felhasználók számára egyszerűbbé tettük a kapcsolatok létrehozását. + No comment provided by engineer. + WebRTC ICE servers WebRTC ICE-kiszolgálók @@ -9152,6 +10078,11 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Ha egy inkognitóprofilt oszt meg valamelyik partnerével, a rendszer ezt az inkognitóprofilt fogja használni azokban a csoportokban, ahová az adott partnere meghívja Önt. No comment provided by engineer. + + Why SimpleX is built. + Miért jött létre a SimpleX? + No comment provided by engineer. + WiFi Wi-Fi @@ -9189,7 +10120,7 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. - Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-továbbítókiszolgálók számára: %@. + Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-átjátszók számára: %@. alert message @@ -9364,6 +10295,11 @@ Megismétli a csatlakozási kérést? A lezárási képernyő értesítési előnézetét az „Értesítések” menüben állíthatja be. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + Megoszthat egy hivatkozást vagy egy QR-kódot – bárki képes lesz csatlakozni a csatornához. + 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. Megoszthat egy hivatkozást vagy QR-kódot – így bárki csatlakozhat a csoporthoz. Ha a csoporthivatkozást később törli, akkor nem fogja elveszíteni a csoport meglévő tagjait. @@ -9409,16 +10345,25 @@ Megismétli a csatlakozási kérést? Ön nem tud üzeneteket küldeni! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + Ön kijelenti, hogy: +- nyilvános csoportokban kizárólag megengedett tartalmakat oszt meg +- tiszteletben tartja a többi felhasználót – nem küld senkinek kéretlen tartalmat + No comment provided by engineer. + + + You connected to the channel via this relay link. + Ön ezen az átjátszóhivatkozáson keresztül kapcsolódott a csatornához. + No comment provided by engineer. + You could not be verified; please try again. Nem sikerült ellenőrizni; próbálja meg újra. No comment provided by engineer. - - You decide who can connect. - Ön dönti el, hogy kivel beszélget. - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -9486,6 +10431,11 @@ Megismétli a kapcsolódási kérést? Ön megkapja az értesítéseket. token info + + You were born without an account + Fiók nélkül születtünk. + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. Csak azután tud üzeneteket küldeni, **miután a kérését elfogadták**. @@ -9521,6 +10471,11 @@ Megismétli a kapcsolódási kérést? Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + Ön nem fog több üzenetet kapni ebből a csatornából. A csevegési előzmények megmaradnak. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak. @@ -9566,6 +10521,11 @@ Megismétli a kapcsolódási kérést? Hívások No comment provided by engineer. + + Your channel + Saját csatorna + No comment provided by engineer. + Your chat database Csevegési adatbázis @@ -9616,6 +10576,11 @@ Megismétli a kapcsolódási kérést? A partnereivel továbbra is kapcsolatban marad. No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + A beszélgetései Önhöz tartoznak, ahogy az internet megjelenése előtt is mindig így volt. A hálózat nem egy hely, amelyet meglátogat. Ez egy olyan hely, amelyet Ön hoz létre saját magának. És senki sem veheti el Öntől, függetlenül attól, hogy privát vagy nyilvános. + No comment provided by engineer. + Your credentials may be sent unencrypted. A hitelesítési adatai titkosítatlanul is elküldhetők. @@ -9636,6 +10601,11 @@ Megismétli a kapcsolódási kérést? Saját csoport No comment provided by engineer. + + Your network + Saját hálózat + No comment provided by engineer. + Your preferences Beállítások @@ -9651,6 +10621,13 @@ Megismétli a kapcsolódási kérést? Saját profil No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + A(z) **%@** nevű profilja meg lesz osztva a csatorna átjátszóival és feliratkozóival. +Az átjátszók hozzáférhetnek a csatornaüzenetekhez. + No comment provided by engineer. + Your profile **%@** will be shared. A(z) **%@** nevű profilja meg lesz osztva. @@ -9671,14 +10648,29 @@ Megismétli a kapcsolódási kérést? A profilja módosult. Ha menti, akkor a profilfrissítés el lesz küldve a partnerei számára. alert message + + Your public address + Saját nyilvános cím + No comment provided by engineer. + Your random profile Véletlenszerű profil No comment provided by engineer. + + Your relay address + Saját átjátszó címe + No comment provided by engineer. + + + Your relay name + Saját átjátszó neve + No comment provided by engineer. + Your server address - Saját SMP-kiszolgálójának címe + Saját SMP-kiszolgáló címe No comment provided by engineer. @@ -9691,21 +10683,11 @@ Megismétli a kapcsolódási kérést? Beállítások No comment provided by engineer. - - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - [Közreműködés](https://github.com/simplex-chat/simplex-chat#contribute) - No comment provided by engineer. - [Send us email](mailto:chat@simplex.chat) [Küldjön nekünk e-mailt](mailto:chat@simplex.chat) No comment provided by engineer. - - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - [Csillagozás a GitHubon](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - \_italic_ \_dőlt_ @@ -9721,6 +10703,11 @@ Megismétli a kapcsolódási kérést? gombra fent, majd válassza ki: No comment provided by engineer. + + accepted + elfogadva + No comment provided by engineer. + accepted %@ befogadta őt: %@ @@ -9741,6 +10728,11 @@ Megismétli a kapcsolódási kérést? befogadta Önt rcv group event chat item + + active + aktív + No comment provided by engineer. + admin adminisztrátor @@ -9852,6 +10844,11 @@ marked deleted chat item preview text hívás… call status + + can't broadcast + nem lehet közvetíteni + No comment provided by engineer. + can't send messages nem lehet üzeneteket küldeni @@ -9887,6 +10884,16 @@ marked deleted chat item preview text cím módosítása… chat item text + + channel + csatorna + shown as sender role for channel messages + + + channel profile updated + csatornaprofil frissítve + snd group event chat item + colored színezett @@ -10033,6 +11040,11 @@ pref value törölve deleted chat item + + deleted channel + törölt csatorna + rcv group event chat item + deleted contact törölt partner @@ -10143,6 +11155,11 @@ pref value hiba No comment provided by engineer. + + error: %@ + hiba: %@ + receive error chat item + expired lejárt @@ -10150,6 +11167,7 @@ pref value failed + sikertelen No comment provided by engineer. @@ -10272,6 +11290,11 @@ pref value elhagyta a csoportot rcv group event chat item + + link + hivatkozás + No comment provided by engineer. + marked deleted törlésre jelölve @@ -10342,6 +11365,11 @@ pref value soha delete after time + + new + új + No comment provided by engineer. + new message új üzenet @@ -10465,6 +11493,11 @@ time to disappear elutasított hívás call status + + relay + átjátszó + member role + removed eltávolítva @@ -10475,6 +11508,16 @@ time to disappear eltávolította őt: %@ rcv group event chat item + + removed (%d attempts) + eltávolítva (%d kísérlet) + receive error chat item + + + removed by operator + az üzemeltető eltávolította + No comment provided by engineer. + removed contact address eltávolította a kapcsolattartási címet @@ -10629,6 +11672,11 @@ utoljára fogadott üzenet: %2$@ nem védett No comment provided by engineer. + + updated channel profile + frissített csatornaprofil + rcv group event chat item + updated group profile frissítette a csoportprofilt @@ -10649,6 +11697,11 @@ utoljára fogadott üzenet: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + a következőn keresztül: %@ + relay hostname + via contact address link a kapcsolattartási címhivatkozáson keresztül @@ -10666,7 +11719,7 @@ utoljára fogadott üzenet: %2$@ via relay - továbbítókiszolgálón keresztül + átjátszón keresztül No comment provided by engineer. @@ -10724,6 +11777,11 @@ utoljára fogadott üzenet: %2$@ Ön megfigyelő No comment provided by engineer. + + you are subscriber + Ön feliratkozó + No comment provided by engineer. + you blocked %@ Ön letiltotta őt: %@ @@ -10784,6 +11842,11 @@ utoljára fogadott üzenet: %2$@ \~áthúzott~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + ⚠️ Nem sikerült ellenőrizni az aláírást: %@. + owner verification + @@ -10798,7 +11861,7 @@ utoljára fogadott üzenet: %2$@ SimpleX needs camera access to scan QR codes to connect to other users and for video calls. - A SimpleXnek kamera-hozzáférésre van szüksége a QR-kódok beolvasásához, hogy kapcsolódhasson más felhasználókhoz és videohívásokhoz. + A SimpleXnek hozzáférésre van szüksége a kamerához a QR-kódok beolvasásához, hogy kapcsolódhasson más felhasználókhoz és videohívásokhoz. Privacy - Camera Usage Description @@ -10813,7 +11876,7 @@ utoljára fogadott üzenet: %2$@ SimpleX needs microphone access for audio and video calls, and to record voice messages. - A SimpleXnek mikrofon-hozzáférésre van szüksége hang- és videohívásokhoz, valamint hangüzenetek rögzítéséhez. + A SimpleXnek hozzáférésre van szüksége a mikrofonhoz a hang- és videohívásokhoz, valamint hangüzenetek rögzítéséhez. Privacy - Microphone Usage Description 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 97061054e8..4d9fce7b4f 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -185,6 +185,24 @@ %d mesi time interval + + %d relays failed + %d relay falliti + channel relay bar +channel subscriber relay bar + + + %d relays not active + %d relay non attivi + channel relay bar +channel subscriber relay bar + + + %d relays removed + %d relay rimossi + channel relay bar +channel subscriber relay bar + %d sec %d sec @@ -200,11 +218,63 @@ %d messaggio/i saltato/i integrity error chat item + + %d subscriber + %d iscritto + channel subscriber count + + + %d subscribers + %d iscritti + channel subscriber count + %d weeks %d settimane time interval + + %1$d/%2$d relays active + %1$d/%2$d relay attivo/i + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + %1$d/%2$d relay attivi, %3$d errori + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + %1$d/%2$d relay attivo/i, %3$d fallito/i + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + %1$d/%2$d relay attivi, %3$d rimossi + channel relay bar + + + %1$d/%2$d relays connected + %1$d/%2$d relay connesso/i + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + %1$d/%2$d relay connesso/i, %3$d errori + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + %1$d/%2$d relay connessi, %3$d falliti + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + %1$d/%2$d relay connessi, %3$d rimossi + channel subscriber relay bar + %lld %lld @@ -215,6 +285,11 @@ %lld %@ No comment provided by engineer. + + %lld channel events + %lld eventi del canale + No comment provided by engineer. + %lld contact(s) selected %lld contatto/i selezionato/i @@ -315,11 +390,21 @@ %u messaggi saltati. No comment provided by engineer. + + (from owner) + (dal proprietario) + chat link info line + (new) (nuovo) No comment provided by engineer. + + (signed) + (firmato) + chat link info line + (this device v%@) (questo dispositivo v%@) @@ -365,6 +450,11 @@ **Scansiona / Incolla link**: per connetterti tramite un link che hai ricevuto. No comment provided by engineer. + + **Test relay** to retrieve its name. + **Prova il relay** per recuperare il suo nome. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Attenzione**: le notifiche push istantanee richiedono una password salvata nel portachiavi. @@ -408,6 +498,15 @@ - e altro ancora! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + - scegli se inviare anteprime dei link. +- previeni il phishing dei collegamenti ipertestuali. +- rimuovi il tracciamento dei link. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -506,6 +605,11 @@ time interval Qualche altra cosa No comment provided by engineer. + + A link for one person to connect + Un link per una persona da connettere + No comment provided by engineer. + A new contact Un contatto nuovo @@ -632,9 +736,9 @@ swipe action Connessioni attive 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. - Aggiungi l'indirizzo al tuo profilo, in modo che i tuoi contatti possano condividerlo con altre persone. L'aggiornamento del profilo verrà inviato ai tuoi contatti. + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. + Aggiungi l'indirizzo al tuo profilo, in modo che i tuoi contatti di SimpleX possano condividerlo con altre persone. L'aggiornamento del profilo verrà inviato ai tuoi contatti di SimpleX. No comment provided by engineer. @@ -702,6 +806,11 @@ swipe action Server dei messaggi aggiunti No comment provided by engineer. + + Adding relays will be supported later. + L'aggiunta di relay verrà supportata prossimamente. + No comment provided by engineer. + Additional accent Principale aggiuntivo @@ -822,6 +931,16 @@ swipe action Tutti gli profili profile dropdown + + All relays failed + Tutti i relay falliti + No comment provided by engineer. + + + All relays removed + Tutti i relay rimossi + No comment provided by engineer. + All reports will be archived for you. Tutte le segnalazioni verranno archiviate per te. @@ -882,6 +1001,11 @@ swipe action Consenti l'eliminazione irreversibile dei messaggi solo se il contatto la consente a te. (24 ore) No comment provided by engineer. + + Allow members to chat with admins. + Consenti ai membri di chattare con gli amministratori. + No comment provided by engineer. + Allow message reactions only if your contact allows them. Consenti reazioni ai messaggi solo se il tuo contatto le consente. @@ -897,6 +1021,11 @@ swipe action Permetti l'invio di messaggi diretti ai membri. No comment provided by engineer. + + Allow sending direct messages to subscribers. + Permetti l'invio di messaggi diretti agli iscritti. + No comment provided by engineer. + Allow sending disappearing messages. Permetti l'invio di messaggi a tempo. @@ -907,6 +1036,11 @@ swipe action Consenti la condivisione No comment provided by engineer. + + Allow subscribers to chat with admins. + Consenti agli iscritti di chattare con gli amministratori. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Permetti di eliminare irreversibilmente i messaggi inviati. (24 ore) @@ -1012,11 +1146,6 @@ swipe action Rispondi alla chiamata No comment provided by engineer. - - Anybody can host servers. - Chiunque può installare i server. - No comment provided by engineer. - App build: %@ Build dell'app: %@ @@ -1194,7 +1323,7 @@ swipe action Auto-accept images - Auto-accetta le immagini + Accetta automaticamente le immagini No comment provided by engineer. @@ -1222,6 +1351,23 @@ swipe action Hash del messaggio errato No comment provided by engineer. + + Be free +in your network + Vivi libero +nella tua rete + No comment provided by engineer. + + + Be free in your network. + Vivi libero nella tua rete. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + Perché abbiamo distrutto il potere di sapere chi sei. In modo che il tuo potere non possa mai esserti sottratto. + No comment provided by engineer. + Better calls Chiamate migliorate @@ -1317,6 +1463,11 @@ swipe action Bloccare il membro? No comment provided by engineer. + + Block subscriber for all? + Bloccare l'iscritto per tutti? + No comment provided by engineer. + Blocked by admin Bloccato dall'amministratore @@ -1367,6 +1518,16 @@ swipe action Sia tu che il tuo contatto potete inviare messaggi vocali. No comment provided by engineer. + + Bottom bar + Barra inferiore + No comment provided by engineer. + + + Broadcast + Trasmetti + compose placeholder for channel owner + 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)! @@ -1375,7 +1536,7 @@ swipe action Business address Indirizzo di lavoro - No comment provided by engineer. + chat link info line Business chats @@ -1397,15 +1558,6 @@ swipe action Per profilo di chat (predefinito) o [per connessione](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - Usando SimpleX Chat accetti di: -- inviare solo contenuto legale nei gruppi pubblici. -- rispettare gli altri utenti - niente spam. - No comment provided by engineer. - Call already ended! Chiamata già terminata! @@ -1554,6 +1706,82 @@ new chat action authentication reason set passcode view + + Channel + Canale + No comment provided by engineer. + + + Channel display name + Nome da mostrare del canale + No comment provided by engineer. + + + Channel full name (optional) + Nome completo del canale (facoltativo) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + Il canale non ha relay attivi. Prova a iscriverti più tardi. + alert message +alert subtitle + + + Channel image + Immagine del canale + No comment provided by engineer. + + + Channel link + Link del canale + chat link info line + + + Channel preferences + Preferenze del canale + No comment provided by engineer. + + + Channel profile + Profilo del canale + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + Il profilo del canale è memorizzato sui dispositivi degli iscritti e sui relay di chat. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + Il profilo del canale è stato cambiato. Se lo salvi, il profilo aggiornato verrà inviato agli iscritti di canale. + alert message + + + Channel temporarily unavailable + Canale non disponibile temporaneamente + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + Il canale verrà eliminato per tutti gli iscritti, non è reversibile! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + Il canale verrà eliminato per te, non è reversibile! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + Il canale sarà operativo con %1$d di %2$d relay. Procedere? + alert message + + + Channels + Canali + No comment provided by engineer. + Chat Chat @@ -1639,6 +1867,26 @@ set passcode view Profilo utente No comment provided by engineer. + + Chat relay + Relay di chat + No comment provided by engineer. + + + Chat relays + Relay di chat + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + I relay di chat inoltrano i messaggi nei canali che crei. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + I relay di chat inoltrano i messaggi agli iscritti del canale. + No comment provided by engineer. + Chat theme Tema della chat @@ -1657,7 +1905,8 @@ set passcode view Chat with admins Chat con amministratori - chat toolbar + chat feature +chat toolbar Chat with member @@ -1674,11 +1923,26 @@ set passcode view Chat No comment provided by engineer. + + Chats with admins are prohibited. + Le chat con gli amministratori sono vietate. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + Le chat con amministratori in canali pubblici non hanno crittografia E2E: usale solo con relay di chat fidati. + alert message + Chats with members Chat con membri No comment provided by engineer. + + Chats with members are disabled + Le chat con i membri sono disattivate + No comment provided by engineer. + Check messages every 20 min. Controlla i messaggi ogni 20 min. @@ -1689,6 +1953,16 @@ set passcode view Controlla i messaggi quando consentito. No comment provided by engineer. + + Check relay address and try again. + Controlla l'indirizzo del relay e riprova. + alert message + + + Check relay name and try again. + Controlla il nome del relay e riprova. + alert message + Check server address and try again. Controlla l'indirizzo del server e riprova. @@ -1834,9 +2108,9 @@ set passcode view Configura server ICE No comment provided by engineer. - - Configure server operators - Configura gli operatori dei server + + Configure relays + Configura i relay No comment provided by engineer. @@ -1897,7 +2171,8 @@ set passcode view Connect Connetti - server test step + relay test step +server test step Connect automatically @@ -1943,6 +2218,11 @@ Questo è il tuo link una tantum! Connetti via link new chat sheet title + + Connect via link or QR code + Connetti via link o codice QR + No comment provided by engineer. + Connect via one-time link Connetti via link una tantum @@ -2021,10 +2301,11 @@ Questo è il tuo link una tantum! Connection error (AUTH) Errore di connessione (AUTH) - No comment provided by engineer. + conn error description Connection failed + Connessione fallita No comment provided by engineer. @@ -2079,6 +2360,11 @@ Questo è il tuo link una tantum! Connessioni No comment provided by engineer. + + Contact address + Indirizzo di contatto + chat link info line + Contact allows Il contatto lo consente @@ -2149,6 +2435,11 @@ Questo è il tuo link una tantum! Continua No comment provided by engineer. + + Contribute + Contribuisci + No comment provided by engineer. + Conversation deleted! Conversazione eliminata! @@ -2177,12 +2468,7 @@ Questo è il tuo link una tantum! Correct name to %@? Correggere il nome a %@? - No comment provided by engineer. - - - Create - Crea - No comment provided by engineer. + alert message Create 1-time link @@ -2234,6 +2520,16 @@ Questo è il tuo link una tantum! Crea profilo No comment provided by engineer. + + Create public channel + Crea canale pubblico + No comment provided by engineer. + + + Create public channel (BETA) + Crea canale pubblico (BETA) + No comment provided by engineer. + Create queue Crea coda @@ -2244,11 +2540,21 @@ Questo è il tuo link una tantum! Crea il tuo indirizzo No comment provided by engineer. + + Create your link + Connettiti con qualcuno + No comment provided by engineer. + Create your profile Crea il tuo profilo No comment provided by engineer. + + Create your public address + Crea il tuo indirizzo pubblico + No comment provided by engineer. + Created Creato @@ -2269,6 +2575,11 @@ Questo è il tuo link una tantum! Creazione link dell'archivio No comment provided by engineer. + + Creating channel + Creazione canale + No comment provided by engineer. + Creating link… Creazione link… @@ -2427,10 +2738,10 @@ Questo è il tuo link una tantum! Debug della consegna No comment provided by engineer. - - Decentralized - Decentralizzato - No comment provided by engineer. + + Decode link + Decodifica il link + relay test step Decryption error @@ -2478,6 +2789,16 @@ swipe action Elimina e avvisa il contatto No comment provided by engineer. + + Delete channel + Elimina canale + No comment provided by engineer. + + + Delete channel? + Eliminare il canale? + No comment provided by engineer. + Delete chat Elimina chat @@ -2649,6 +2970,11 @@ alert button Elimina coda server test step + + Delete relay + Elimina relay + No comment provided by engineer. + Delete report Elimina la segnalazione @@ -2814,6 +3140,16 @@ alert button I messaggi diretti tra i membri sono vietati in questo gruppo. No comment provided by engineer. + + Direct messages between subscribers are prohibited. + I messaggi diretti tra gli iscritti sono vietati. + No comment provided by engineer. + + + Disable + Disattiva + alert button + Disable (keep overrides) Disattiva (mantieni sostituzioni) @@ -2919,6 +3255,11 @@ alert button Non inviare la cronologia ai nuovi membri. No comment provided by engineer. + + Do not send history to new subscribers. + Non inviare la cronologia ai nuovi iscritti. + No comment provided by engineer. + Do not use credentials with proxy. Non usare credenziali con proxy. @@ -3020,11 +3361,21 @@ chat item action Notifiche crittografate E2E. No comment provided by engineer. + + Easier to invite your friends 👋 + È più facile invitare i tuoi amici 👋 + No comment provided by engineer. + Edit Modifica chat item action + + Edit channel profile + Modifica profilo canale + No comment provided by engineer. + Edit group profile Modifica il profilo del gruppo @@ -3038,7 +3389,7 @@ chat item action Enable Attiva - No comment provided by engineer. + alert button Enable (keep overrides) @@ -3060,6 +3411,11 @@ chat item action Attiva il keep-alive TCP No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + Attiva almeno un relay di chat in "Rete e server". + channel creation warning + Enable automatic message deletion? Attivare l'eliminazione automatica dei messaggi? @@ -3070,6 +3426,11 @@ chat item action Attiva l'accesso alla fotocamera No comment provided by engineer. + + Enable chats with admins? + Attivare le chat con gli amministratori? + alert title + Enable disappearing messages by default. Attiva i messaggi a tempo in modo predefinito. @@ -3090,16 +3451,16 @@ chat item action Attivare le notifiche istantanee? No comment provided by engineer. + + Enable link previews? + Attivare le anteprime dei link? + alert title + Enable lock Attiva blocco No comment provided by engineer. - - Enable notifications - Attiva le notifiche - No comment provided by engineer. - Enable periodic notifications? Attivare le notifiche periodiche? @@ -3205,6 +3566,11 @@ chat item action Inserisci il codice di accesso No comment provided by engineer. + + Enter channel name… + Inserisci il nome del canale… + No comment provided by engineer. + Enter correct passphrase. Inserisci la password giusta. @@ -3230,6 +3596,16 @@ chat item action Inserisci la password sopra per mostrare! No comment provided by engineer. + + Enter profile name... + Inserisci nome profilo... + No comment provided by engineer. + + + Enter relay name… + Inserisci il nome del relay… + No comment provided by engineer. + Enter server manually Inserisci il server a mano @@ -3258,7 +3634,7 @@ chat item action Error Errore - No comment provided by engineer. + conn error description Error aborting address change @@ -3285,6 +3661,11 @@ chat item action Errore di aggiunta membro/i No comment provided by engineer. + + Error adding relay + Errore di aggiunta del relay + alert title + Error adding server Errore di aggiunta del server @@ -3345,6 +3726,11 @@ chat item action Errore nella creazione dell'indirizzo No comment provided by engineer. + + Error creating channel + Errore di creazione del canale + alert title + Error creating group Errore nella creazione del gruppo @@ -3480,11 +3866,6 @@ chat item action Errore di apertura della chat No comment provided by engineer. - - Error opening group - Errore di preparazione del gruppo - No comment provided by engineer. - Error receiving file Errore nella ricezione del file @@ -3530,6 +3911,11 @@ chat item action Errore nel salvataggio dei server ICE No comment provided by engineer. + + Error saving channel profile + Errore di salvataggio del profilo del canale + No comment provided by engineer. + Error saving chat list Errore nel salvataggio dell'elenco di chat @@ -3595,6 +3981,11 @@ chat item action Errore nell'impostazione delle ricevute di consegna! No comment provided by engineer. + + Error sharing channel + Errore nella condivisione del canale + alert title + Error starting chat Errore di avvio della chat @@ -3675,7 +4066,8 @@ snd error text Error: %@. Errore: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3919,7 +4311,8 @@ snd error text Fingerprint in server address does not match certificate. L'impronta digitale nell'indirizzo del server non corrisponde al certificato. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3961,10 +4354,16 @@ snd error text Per tutti i moderatori No comment provided by engineer. + + For anyone to reach you + Per chiunque debba raggiungerti + No comment provided by engineer. + For chat profile %@: Per il profilo di chat %@: - servers error + servers error +servers warning For console @@ -4105,11 +4504,21 @@ Errore: %2$@ GIF e adesivi No comment provided by engineer. + + Get link + Ottieni link + relay test step + Get notified when mentioned. Ricevi una notifica quando menzionato. No comment provided by engineer. + + Get started + Cominciamo + No comment provided by engineer. + Good afternoon! Buon pomeriggio! @@ -4168,7 +4577,7 @@ Errore: %2$@ Group link Link del gruppo - No comment provided by engineer. + chat link info line Group links @@ -4280,6 +4689,11 @@ Errore: %2$@ La cronologia non viene inviata ai nuovi membri. No comment provided by engineer. + + History is not sent to new subscribers. + La cronologia non viene inviata ai nuovi iscritti. + No comment provided by engineer. + How SimpleX works Come funziona SimpleX @@ -4347,6 +4761,7 @@ Errore: %2$@ If you joined or created channels, they will stop working permanently. + Se sei dentro canali o ne hai creati, essi smetteranno di funzionare definitivamente. down migration warning @@ -4379,11 +4794,6 @@ Errore: %2$@ Immediatamente No comment provided by engineer. - - Immune to spam - Immune a spam e abusi - No comment provided by engineer. - Import Importa @@ -4526,9 +4936,9 @@ Altri miglioramenti sono in arrivo! Ruolo iniziale No comment provided by engineer. - - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - Installa [Simplex Chat per terminale](https://github.com/simplex-chat/simplex-chat) + + Install SimpleX Chat for terminal + Installa Simplex Chat per terminale No comment provided by engineer. @@ -4586,7 +4996,7 @@ Altri miglioramenti sono in arrivo! Invalid connection link Link di connessione non valido - No comment provided by engineer. + conn error description Invalid display name! @@ -4606,7 +5016,17 @@ Altri miglioramenti sono in arrivo! Invalid name! Nome non valido! - No comment provided by engineer. + alert title + + + Invalid relay address! + Indirizzo del relay non valido! + alert title + + + Invalid relay name! + Nome del relay non valido! + alert title Invalid response @@ -4643,6 +5063,11 @@ Altri miglioramenti sono in arrivo! Invita membri No comment provided by engineer. + + Invite someone privately + Invita qualcuno in modo privato + No comment provided by engineer. + Invite to chat Invita in chat @@ -4719,6 +5144,11 @@ Altri miglioramenti sono in arrivo! entra come %@ No comment provided by engineer. + + Join channel + Iscriviti al canale + No comment provided by engineer. + Join group Entra nel gruppo @@ -4806,6 +5236,16 @@ Questo è il tuo link per il gruppo %@! Esci swipe action + + Leave channel + Esci dal canale + No comment provided by engineer. + + + Leave channel? + Uscire dal canale? + No comment provided by engineer. + Leave chat Esci dalla chat @@ -4831,6 +5271,11 @@ Questo è il tuo link per il gruppo %@! Meno traffico sulle reti mobili. No comment provided by engineer. + + Let someone connect to you + Lascia che qualcuno si connetta a te + No comment provided by engineer. + Let's talk in SimpleX Chat Parliamo in SimpleX Chat @@ -4851,6 +5296,11 @@ Questo è il tuo link per il gruppo %@! Collega le app mobile e desktop! 🔗 No comment provided by engineer. + + Link signature verified. + Firma del link verificata. + owner verification + Linked desktop options Opzioni del desktop collegato @@ -5036,6 +5486,11 @@ Questo è il tuo link per il gruppo %@! I membri del gruppo possono aggiungere reazioni ai messaggi. No comment provided by engineer. + + Members can chat with admins. + I membri possono chattare con gli amministratori. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore) @@ -5101,6 +5556,11 @@ Questo è il tuo link per il gruppo %@! Bozza del messaggio No comment provided by engineer. + + Message error + Errore del messaggio + No comment provided by engineer. + Message forwarded Messaggio inoltrato @@ -5196,6 +5656,16 @@ Questo è il tuo link per il gruppo %@! I messaggi da %@ verranno mostrati! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + I messaggi in questo canale **non sono crittografati end-to-end**. I relay di chat possono vedere questi messaggi. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + I messaggi in questo canale non sono crittografati end-to-end. I relay di chat possono vedere questi messaggi. + E2EE info chat item + Messages in this chat will never be deleted. I messaggi in questa chat non verranno mai eliminati. @@ -5226,16 +5696,16 @@ Questo è il tuo link per il gruppo %@! I messaggi, i file e le chiamate sono protetti da **crittografia e2e resistente alla quantistica** con perfect forward secrecy, ripudio e recupero da intrusione. No comment provided by engineer. + + Migrate + Migra + No comment provided by engineer. + Migrate device Migra dispositivo No comment provided by engineer. - - Migrate from another device - Migra da un altro dispositivo - No comment provided by engineer. - Migrate here Migra qui @@ -5356,6 +5826,11 @@ Questo è il tuo link per il gruppo %@! Rete e server No comment provided by engineer. + + Network commitments + Impegni sulla rete + No comment provided by engineer. + Network connection Connessione di rete @@ -5366,6 +5841,11 @@ Questo è il tuo link per il gruppo %@! Decentralizzazione della rete No comment provided by engineer. + + Network error + Errore di rete + conn error description + Network issues - message expired after many attempts to send it. Problemi di rete - messaggio scaduto dopo molti tentativi di inviarlo. @@ -5381,6 +5861,13 @@ Questo è il tuo link per il gruppo %@! Operatore di rete No comment provided by engineer. + + Network routers cannot know +who talks to whom + Gli instradatori di rete non possono +sapere chi parla con chi + No comment provided by engineer. + Network settings Impostazioni di rete @@ -5396,6 +5883,11 @@ Questo è il tuo link per il gruppo %@! Nuovo token status text + + New 1-time link + Nuovo link una tantum + No comment provided by engineer. + New Passcode Nuovo codice di accesso @@ -5421,6 +5913,11 @@ Questo è il tuo link per il gruppo %@! Una nuova esperienza di chat 🎉 No comment provided by engineer. + + New chat relay + Nuovo relay di chat + No comment provided by engineer. + New contact request Nuova richiesta di contatto @@ -5491,11 +5988,33 @@ Questo è il tuo link per il gruppo %@! No No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + Nessun account. Nessun telefono. Nessuna email. Nessun identificatore. +La crittografia più sicura. + No comment provided by engineer. + + + No active relays + Nessun relay attivo + No comment provided by engineer. + No app password Nessuna password dell'app Authentication unavailable + + No chat relays + Nessun relay di chat + No comment provided by engineer. + + + No chat relays enabled. + Nessun relay di chat attivato. + servers warning + No chats Nessuna chat @@ -5641,11 +6160,26 @@ Questo è il tuo link per il gruppo %@! Nessuna chat non letta No comment provided by engineer. - - No user identifiers. - Nessun identificatore utente. + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. + Nessuno monitorava le tue conversazioni. Nessuno disegnava una mappa delle tue posizioni. La privacy non era mai stata una caratteristica, era uno stile di vita. No comment provided by engineer. + + Non-profit governance + Organizzazione non a scopo di lucro + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + Non una serratura migliore sulla porta di qualcun altro. Non un padrone di casa più gentile che rispetta la tua privacy, ma che continua a tenere traccia di tutti i visitatori. Non sei un ospite. Sei a casa tua. Nessun re può entrarvi: sei tu il sovrano. + No comment provided by engineer. + + + Not all relays connected + Non tutti i relay sono connessi + alert title + Not compatible! Non compatibile! @@ -5703,7 +6237,7 @@ Questo è il tuo link per il gruppo %@! OK OK - No comment provided by engineer. + alert button Off @@ -5722,11 +6256,21 @@ new chat action Database vecchio No comment provided by engineer. + + On your phone, not on servers. + Sul tuo telefono, non sui server. + No comment provided by engineer. + One-time invitation link Link di invito una tantum No comment provided by engineer. + + One-time link + Link una tantum + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5746,6 +6290,11 @@ Richiede l'attivazione della VPN. Gli host Onion non verranno usati. No comment provided by engineer. + + Only channel owners can change channel preferences. + Solo i proprietari del canale possono modificarne le preferenze. + No comment provided by engineer. + Only chat owners can change preferences. Solo i proprietari della chat possono modificarne le preferenze. @@ -5849,7 +6398,8 @@ Richiede l'attivazione della VPN. Open Apri - alert action + alert action +alert button Open Settings @@ -5861,6 +6411,11 @@ Richiede l'attivazione della VPN. Apri le modifiche No comment provided by engineer. + + Open channel + Apri canale + new chat action + Open chat Apri chat @@ -5881,6 +6436,11 @@ Richiede l'attivazione della VPN. Apri le condizioni No comment provided by engineer. + + Open external link? + Aprire il link esterno? + alert title + Open full link Apri link completo @@ -5901,6 +6461,11 @@ Richiede l'attivazione della VPN. Apri migrazione ad un altro dispositivo authentication reason + + Open new channel + Apri un canale nuovo + new chat action + Open new chat Apri una chat nuova @@ -5946,14 +6511,25 @@ Richiede l'attivazione della VPN. Server dell'operatore alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + Gli operatori si impegnano a: +- Essere indipendenti +- Minimizzare l'uso di metadati +- Eseguire codice open source verificato + No comment provided by engineer. + Or import archive file - O importa file archivio + O importa un file dell'archivio No comment provided by engineer. Or paste archive link - O incolla il link dell'archivio + O incolla un link dell'archivio No comment provided by engineer. @@ -5966,6 +6542,11 @@ Richiede l'attivazione della VPN. O condividi in modo sicuro questo link del file No comment provided by engineer. + + Or show QR in person or via video call. + O mostra il QR di persona o via videochiamata. + No comment provided by engineer. + Or show this code O mostra questo codice @@ -5976,6 +6557,11 @@ Richiede l'attivazione della VPN. O per condividere in modo privato No comment provided by engineer. + + Or use this QR - print or show online. + O usa questo QR: stampalo o mostralo online. + No comment provided by engineer. + Organize chats into lists Organizza le chat in elenchi @@ -5993,6 +6579,21 @@ Richiede l'attivazione della VPN. %@ alert message + + Owner + Proprietario + No comment provided by engineer. + + + Owners + Proprietari + No comment provided by engineer. + + + Ownership: you can run your own relays. + Proprietà: puoi gestire i tuoi relay personali. + No comment provided by engineer. + PING count Conteggio PING @@ -6048,6 +6649,11 @@ Richiede l'attivazione della VPN. Incolla immagine No comment provided by engineer. + + Paste link / Scan + Incolla link / Scansiona + No comment provided by engineer. + Paste link to connect! Incolla un link per connettere! @@ -6202,6 +6808,16 @@ Errore: %@ Conserva la bozza dell'ultimo messaggio, con gli allegati. No comment provided by engineer. + + Preset relay address + Indirizzo relay preimpostato + No comment provided by engineer. + + + Preset relay name + Nome relay preimpostato + No comment provided by engineer. + Preset server address Indirizzo server preimpostato @@ -6237,14 +6853,14 @@ Errore: %@ Informativa sulla privacy e condizioni d'uso. No comment provided by engineer. - - Privacy redefined - Privacy ridefinita + + Privacy: for owners and subscribers. + Privacy: per i proprietari e gli iscritti. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. - Le chat private, i gruppi e i tuoi contatti non sono accessibili agli operatori dei server. + + Private and secure messaging. + Messaggistica privata e sicura. No comment provided by engineer. @@ -6287,6 +6903,11 @@ Errore: %@ Scadenza dell'instradamento privato alert title + + Proceed + Procedi + alert action + Profile and server connections Profilo e connessioni al server @@ -6312,9 +6933,9 @@ Errore: %@ Tema del profilo No comment provided by engineer. - - Profile update will be sent to your contacts. - L'aggiornamento del profilo verrà inviato ai tuoi contatti. + + Profile update will be sent to your SimpleX contacts. + L'aggiornamento del profilo verrà inviato ai tuoi contatti di SimpleX. alert message @@ -6322,6 +6943,11 @@ Errore: %@ Proibisci le chiamate audio/video. No comment provided by engineer. + + Prohibit chats with admins. + Vieta le chat con gli amministratori. + No comment provided by engineer. + Prohibit irreversible message deletion. Proibisci l'eliminazione irreversibile dei messaggi. @@ -6352,6 +6978,11 @@ Errore: %@ Proibisci l'invio di messaggi diretti ai membri. No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + Proibisci l'invio di messaggi diretti agli iscritti. + No comment provided by engineer. + Prohibit sending disappearing messages. Proibisci l'invio di messaggi a tempo. @@ -6419,6 +7050,11 @@ Attivalo nelle impostazioni *Rete e server*. Il proxy richiede una password No comment provided by engineer. + + Public channels - speak freely 🚀 + Canali pubblici - parla liberamente 🚀 + No comment provided by engineer. + Push notifications Notifiche push @@ -6459,24 +7095,14 @@ Attivalo nelle impostazioni *Rete e server*. Leggi tutto No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - Leggi di più nella [Guida utente](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + Leggi di più nella Guida utente. No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - No comment provided by engineer. - - - Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - Maggiori informazioni nella [Guida per l'utente](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). - Maggiori informazioni nel nostro [repository GitHub](https://github.com/simplex-chat/simplex-chat#readme). + + Read more in our GitHub repository. + Maggiori informazioni nel nostro repository GitHub. No comment provided by engineer. @@ -6636,6 +7262,31 @@ swipe action Rifiutare il membro? alert title + + Relay + Relay + No comment provided by engineer. + + + Relay address + Indirizzo del relay + alert title + + + Relay connection failed + Connessione del relay fallita + alert title + + + Relay link + Link del relay + No comment provided by engineer. + + + Relay results: + Risultati relay: + alert message + Relay server is only used if necessary. Another party can observe your IP address. Il server relay viene usato solo se necessario. Un altro utente può osservare il tuo indirizzo IP. @@ -6646,6 +7297,16 @@ swipe action Il server relay protegge il tuo indirizzo IP, ma può osservare la durata della chiamata. No comment provided by engineer. + + Relay test failed! + Prova del relay fallita! + No comment provided by engineer. + + + Reliability: many relays per channel. + Affidabilità: relay multipli per canale. + No comment provided by engineer. + Remove Rimuovi @@ -6686,6 +7347,16 @@ swipe action Rimuovere la password dal portachiavi? No comment provided by engineer. + + Remove subscriber + Rimuovi iscritto + No comment provided by engineer. + + + Remove subscriber? + Rimuovere l'iscritto? + alert title + Removes messages and blocks members. Rimuove i messaggi e blocca i membri. @@ -6921,6 +7592,11 @@ swipe action Proxy SOCKS No comment provided by engineer. + + Safe web links + Link web sicuri + No comment provided by engineer. + Safely receive files Ricevi i file in sicurezza @@ -6944,7 +7620,12 @@ chat item action Save (and notify members) - Salva (e informa i membri) + Salva (e avvisa i membri) + alert button + + + Save (and notify subscribers) + Salva (e avvisa gli iscritti) alert button @@ -6962,6 +7643,11 @@ chat item action Salva e avvisa i membri del gruppo No comment provided by engineer. + + Save and notify subscribers + Salva e avvisa gli iscritti + No comment provided by engineer. + Save and reconnect Salva e riconnetti @@ -6972,6 +7658,16 @@ chat item action Salva e aggiorna il profilo del gruppo No comment provided by engineer. + + Save channel profile + Salva il profilo del canale + No comment provided by engineer. + + + Save channel profile? + Salva il profilo del canale? + alert title + Save group profile Salva il profilo del gruppo @@ -7064,7 +7760,7 @@ chat item action Scan QR code - Scansiona codice QR + Scansiona un codice QR No comment provided by engineer. @@ -7152,6 +7848,11 @@ chat item action Codice di sicurezza No comment provided by engineer. + + Security: owners hold channel keys. + Sicurezza: solo i proprietari hanno le chiavi del canale. + No comment provided by engineer. + Select Seleziona @@ -7282,6 +7983,11 @@ chat item action Invia richiesta senza messaggio No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + Invia il link tramite qualsiasi messenger, è sicuro. Chiedi di incollarlo in SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. Inviali dalla galleria o dalle tastiere personalizzate. @@ -7292,6 +7998,11 @@ chat item action Invia fino a 100 ultimi messaggi ai nuovi membri. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + Invia fino a 100 ultimi messaggi ai nuovi iscritti. + No comment provided by engineer. + Send your private feedback to groups. Invia i tuoi commenti privati ai gruppi. @@ -7307,6 +8018,11 @@ chat item action Il mittente potrebbe aver eliminato la richiesta di connessione. No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + L'invio di un'anteprima del link può rivelare il tuo indirizzo IP al sito. Puoi modificarlo nelle impostazioni di Privacy più tardi. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. L'invio delle ricevute di consegna sarà attivo per tutti i contatti in tutti i profili di chat visibili. @@ -7432,6 +8148,11 @@ chat item action Il protocollo del server è cambiato. alert title + + Server requires authorization to connect to relay, check password. + Il server richiede l'autorizzazione per connettersi al relay, controlla la password. + relay test error + Server requires authorization to create queues, check password. Il server richiede l'autorizzazione di creare code, controlla la password. @@ -7562,6 +8283,16 @@ chat item action Le impostazioni sono state cambiate. alert message + + Setup notifications + Configura le notifiche + No comment provided by engineer. + + + Setup routers + Configura gli instradatori + No comment provided by engineer. + Shape profile images Forma delle immagini del profilo @@ -7598,11 +8329,16 @@ chat item action Condividi indirizzo pubblicamente No comment provided by engineer. - - Share address with contacts? - Condividere l'indirizzo con i contatti? + + Share address with SimpleX contacts? + Condividere l'indirizzo con i contatti di SimpleX? alert title + + Share channel + Condividi canale + No comment provided by engineer. + Share from other apps. Condividi da altre app. @@ -7628,6 +8364,11 @@ chat item action Condividi il profilo No comment provided by engineer. + + Share relay address + Condividi l'indirizzo del relay + No comment provided by engineer. + Share this 1-time invite link Condividi questo link di invito una tantum @@ -7638,9 +8379,14 @@ chat item action Condividi in SimpleX No comment provided by engineer. - - Share with contacts - Condividi con i contatti + + Share via chat + Condividi via chat + No comment provided by engineer. + + + Share with SimpleX contacts + Condividi con i contatti di SimpleX No comment provided by engineer. @@ -7813,9 +8559,9 @@ chat item action Protocolli di SimpleX esaminati da Trail of Bits. No comment provided by engineer. - - SimpleX relay link - Link del relay SimpleX + + SimpleX relay address + Indirizzo del relay SimpleX simplex link type @@ -7891,6 +8637,11 @@ report reason Quadrata, circolare o qualsiasi forma tra le due. No comment provided by engineer. + + Star on GitHub + Dai una stella su GitHub + No comment provided by engineer. + Start chat Avvia chat @@ -7988,9 +8739,81 @@ report reason Subscribed + Iscritto/a + No comment provided by engineer. + + + Subscriber Iscritto No comment provided by engineer. + + Subscriber reports + Segnalazioni degli iscritti + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + L'iscritto verrà rimosso dal canale, non è reversibile! + alert message + + + Subscribers + Iscritti + No comment provided by engineer. + + + Subscribers can add message reactions. + Gli iscritti al canale possono aggiungere reazioni ai messaggi. + No comment provided by engineer. + + + Subscribers can chat with admins. + Gli iscritti possono chattare con gli amministratori. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + Gli iscritti al canale possono eliminare irreversibilmente i messaggi inviati. (24 ore) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + Gli iscritti possono segnalare messaggi ai moderatori. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + Gli iscritti al canale possono inviare link di Simplex. + No comment provided by engineer. + + + Subscribers can send direct messages. + Gli iscritti al canale possono inviare messaggi diretti. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + Gli iscritti al canale possono inviare messaggi a tempo. + No comment provided by engineer. + + + Subscribers can send files and media. + Gli iscritti al canale possono inviare file e contenuti multimediali. + No comment provided by engineer. + + + Subscribers can send voice messages. + Gli iscritti al canale possono inviare messaggi vocali. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + Gli iscritti usano il link del relay per connettersi al canale. +L'indirizzo del relay è stato usato per impostare questo relay per il canale. + No comment provided by engineer. + Subscription errors Errori di iscrizione @@ -8071,6 +8894,11 @@ report reason Scatta foto No comment provided by engineer. + + Talk to someone + Parla con qualcuno + No comment provided by engineer. + Tap Connect to chat Tocca Connetti per chattare @@ -8086,9 +8914,9 @@ report reason Tocca Connetti per usare il bot No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. - Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi. + + Tap Join channel + Tocca Iscriviti al canale No comment provided by engineer. @@ -8121,6 +8949,11 @@ report reason Toccare per entrare in incognito No comment provided by engineer. + + Tap to open + Tocca per aprire + No comment provided by engineer. + Tap to paste link Tocca per incollare il link @@ -8139,13 +8972,19 @@ report reason Test failed at step %@. Test fallito al passo %@. - server test failure + relay test failure +server test failure Test notifications Prova le notifiche No comment provided by engineer. + + Test relay + Prova relay + No comment provided by engineer. + Test server Prova server @@ -8198,6 +9037,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.L'app protegge la tua privacy usando diversi operatori in ogni conversazione. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + L'app ha rimosso questo messaggio dopo %lld tentativi di riceverlo. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). L'app chiederà di confermare i download da server di file sconosciuti (eccetto .onion). @@ -8213,6 +9057,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Il codice che hai scansionato non è un codice QR di link SimpleX. No comment provided by engineer. + + The connection reached the limit of undelivered messages + La connessione ha raggiunto il limite di messaggi non consegnati + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. La connessione ha raggiunto il limite di messaggi non consegnati, il contatto potrebbe essere offline. @@ -8238,9 +9087,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.La crittografia funziona e il nuovo accordo sulla crittografia non è richiesto. Potrebbero verificarsi errori di connessione! No comment provided by engineer. - - The future of messaging - La nuova generazione di messaggistica privata + + The first network where you own +your contacts and groups. + La prima rete in cui possiedi +i tuoi contatti e i tuoi gruppi. No comment provided by engineer. @@ -8278,6 +9129,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Il database vecchio non è stato rimosso durante la migrazione, può essere eliminato. No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + La più antica libertà umana, parlare con un'altra persona senza essere osservati, si basa su un'infrastruttura che non può tradirla. + No comment provided by engineer. + The same conditions will apply to operator **%@**. Le stesse condizioni si applicheranno all'operatore **%@**. @@ -8323,6 +9179,16 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Temi No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + Poi ci siamo trasferiti online e ogni piattaforma ha chiesto un pezzo di noi: il nome, il numero, gli amici. Abbiamo accettato che il prezzo da pagare per comunicare con gli altri fosse quello di far sapere a qualcuno con chi parliamo. Ogni generazione, sia di persone che di tecnologia, ha funzionato così: telefono, email, messenger, social media. Sembrava l'unico modo possibile. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + C'è un'altra via. Una rete senza numeri di telefono. Senza nomi utente. Senza account. Senza identificatori utente di alcun tipo. Una rete che connette le persone e trasferisce messaggi crittografati senza sapere chi è connesso. + No comment provided by engineer. + These conditions will also apply for: **%@**. Queste condizioni si applicheranno anche per: **%@**. @@ -8388,6 +9254,16 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Questo gruppo non esiste più. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + Questo è un indirizzo di relay di chat, non può essere usato per connettersi. + alert message + + + This is your link for channel %@! + Questo è il tuo link per il canale %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Questo link richiede una versione più recente dell'app. Aggiornala o chiedi al tuo contatto di inviare un link compatibile. @@ -8438,6 +9314,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Per nascondere messaggi indesiderati. No comment provided by engineer. + + To make SimpleX Network last. + Per la sostenibilità della rete di SimpleX. + No comment provided by engineer. + To make a new connection Per creare una nuova connessione @@ -8525,11 +9406,6 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio 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 chat list: - Cambia l'elenco delle chat: - No comment provided by engineer. - Toggle incognito when connecting. Attiva/disattiva l'incognito quando ti colleghi. @@ -8545,6 +9421,11 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Opacità barra degli strumenti No comment provided by engineer. + + Top bar + Barra superiore + No comment provided by engineer. + Total Totale @@ -8610,6 +9491,11 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Sbloccare il membro? No comment provided by engineer. + + Unblock subscriber for all? + Sbloccare l'iscritto per tutti? + No comment provided by engineer. + Undelivered messages Messaggi non consegnati @@ -8710,13 +9596,18 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Unsupported connection link Link di connessione non supportato - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. Vengono inviati ai nuovi membri fino a 100 ultimi messaggi. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + Vengono inviati ai nuovi iscritti fino a 100 ultimi messaggi. + No comment provided by engineer. + Update Aggiorna @@ -8842,11 +9733,6 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Usa la porta TCP 443 solo per i server preimpostati. No comment provided by engineer. - - Use chat - Usa la chat - No comment provided by engineer. - Use current profile Usa il profilo attuale @@ -8862,6 +9748,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Usa per i messaggi No comment provided by engineer. + + Use for new channels + Usa per canali nuovi + No comment provided by engineer. + Use for new connections Usa per connessioni nuove @@ -8902,6 +9793,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Usa l'instradamento privato con server sconosciuti. No comment provided by engineer. + + Use relay + Usa relay + No comment provided by engineer. + Use server Usa il server @@ -8922,6 +9818,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Usa l'app con una mano sola. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + Usa questo indirizzo nel tuo profilo di social media, sito web o firma email. + No comment provided by engineer. + Use web port Usa porta web @@ -8942,6 +9843,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Utilizzo dei server SimpleX Chat. No comment provided by engineer. + + Verify + Verifica + relay test step + Verify code with desktop Verifica il codice con il desktop @@ -9062,6 +9968,21 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Messaggio vocale… No comment provided by engineer. + + Wait + Attendi + alert action + + + Wait response + Attendi risposta + relay test step + + + Waiting for channel owner to add relays. + In attesa che il proprietario del canale aggiunga dei relay. + No comment provided by engineer. + Waiting for desktop... In attesa del desktop... @@ -9102,6 +10023,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Attenzione: potresti perdere alcuni dati! No comment provided by engineer. + + We made connecting simpler for new users. + Abbiamo semplificato la connessione per i nuovi utenti. + No comment provided by engineer. + WebRTC ICE servers Server WebRTC ICE @@ -9152,6 +10078,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Quando condividi un profilo in incognito con qualcuno, questo profilo verrà utilizzato per i gruppi a cui ti invitano. No comment provided by engineer. + + Why SimpleX is built. + Perché costruiamo SimpleX. + No comment provided by engineer. + WiFi WiFi @@ -9364,6 +10295,11 @@ Ripetere la richiesta di ingresso? Puoi impostare l'anteprima della notifica nella schermata di blocco tramite le impostazioni. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + Puoi condividere un link o un codice QR, chiunque sarà in grado di iscriversi al canale. + 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. Puoi condividere un link o un codice QR: chiunque potrà unirsi al gruppo. Non perderai i membri del gruppo se in seguito lo elimini. @@ -9409,16 +10345,25 @@ Ripetere la richiesta di ingresso? Non puoi inviare messaggi! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + Tu ti impegni a: +- Pubblicare solo contenuto legale nei gruppi pubblici +- Rispettare gli altri utenti. Niente spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + Ti sei connesso/a al canale attraverso questo link del relay. + No comment provided by engineer. + You could not be verified; please try again. Non è stato possibile verificarti, riprova. No comment provided by engineer. - - You decide who can connect. - Sei tu a decidere chi può connettersi. - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -9486,6 +10431,11 @@ Ripetere la richiesta di connessione? Dovresti ricevere le notifiche. token info + + You were born without an account + Sei nato senza un account + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. Potrai inviare messaggi **solo dopo che la tua richiesta verrà accettata**. @@ -9521,6 +10471,11 @@ Ripetere la richiesta di connessione? Continuerai a ricevere chiamate e notifiche da profili silenziati quando sono attivi. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + Smetterai di ricevere messaggi da questo canale. La cronologia della chat sarà preservata. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Non riceverai più messaggi da questa chat. La cronologia della chat verrà conservata. @@ -9566,6 +10521,11 @@ Ripetere la richiesta di connessione? Le tue chiamate No comment provided by engineer. + + Your channel + Il tuo canale + No comment provided by engineer. + Your chat database Il tuo database della chat @@ -9616,6 +10576,11 @@ Ripetere la richiesta di connessione? I tuoi contatti resteranno connessi. No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + Le tue conversazioni appartengono a te, come è sempre stato prima dell'avvento di internet. La rete non è un luogo che visiti. È un luogo che crei e possiedi. E nessuno può portartelo via, che tu lo renda privato o pubblico. + No comment provided by engineer. + Your credentials may be sent unencrypted. Le credenziali potrebbero essere inviate in chiaro. @@ -9636,6 +10601,11 @@ Ripetere la richiesta di connessione? Il tuo gruppo No comment provided by engineer. + + Your network + La tua rete + No comment provided by engineer. + Your preferences Le tue preferenze @@ -9651,6 +10621,13 @@ Ripetere la richiesta di connessione? Il tuo profilo No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + Il tuo profilo **%@** verrà condiviso con i relay e gli iscritti. +I relay hanno accesso ai messaggi del canale. + No comment provided by engineer. + Your profile **%@** will be shared. Verrà condiviso il tuo profilo **%@**. @@ -9671,11 +10648,26 @@ Ripetere la richiesta di connessione? Il tuo profilo è stato cambiato. Se lo salvi, il profilo aggiornato verrà inviato a tutti i tuoi contatti. alert message + + Your public address + Il tuo indirizzo pubblico + No comment provided by engineer. + Your random profile Il tuo profilo casuale No comment provided by engineer. + + Your relay address + L'indirizzo del tuo relay + No comment provided by engineer. + + + Your relay name + Il nome del tuo relay + No comment provided by engineer. + Your server address L'indirizzo del tuo server @@ -9691,21 +10683,11 @@ Ripetere la richiesta di connessione? Le tue impostazioni No comment provided by engineer. - - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - [Contribuisci](https://github.com/simplex-chat/simplex-chat#contribute) - No comment provided by engineer. - [Send us email](mailto:chat@simplex.chat) [Inviaci un'email](mailto:chat@simplex.chat) No comment provided by engineer. - - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - [Dai una stella su GitHub](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - \_italic_ \_corsivo_ @@ -9721,6 +10703,11 @@ Ripetere la richiesta di connessione? sopra, quindi scegli: No comment provided by engineer. + + accepted + accettato + No comment provided by engineer. + accepted %@ %@ accettato @@ -9741,6 +10728,11 @@ Ripetere la richiesta di connessione? ti ha accettato/a rcv group event chat item + + active + attivo + No comment provided by engineer. + admin amministratore @@ -9852,6 +10844,11 @@ marked deleted chat item preview text chiamata… call status + + can't broadcast + impossibile trasmettere + No comment provided by engineer. + can't send messages impossibile inviare messaggi @@ -9887,6 +10884,16 @@ marked deleted chat item preview text cambio indirizzo… chat item text + + channel + canale + shown as sender role for channel messages + + + channel profile updated + profilo del canale aggiornato + snd group event chat item + colored colorato @@ -10033,6 +11040,11 @@ pref value eliminato deleted chat item + + deleted channel + canale eliminato + rcv group event chat item + deleted contact contatto eliminato @@ -10143,6 +11155,11 @@ pref value errore No comment provided by engineer. + + error: %@ + errore: %@ + receive error chat item + expired scaduto @@ -10150,6 +11167,7 @@ pref value failed + fallito No comment provided by engineer. @@ -10272,6 +11290,11 @@ pref value è uscito/a rcv group event chat item + + link + link + No comment provided by engineer. + marked deleted contrassegnato eliminato @@ -10342,6 +11365,11 @@ pref value mai delete after time + + new + nuovo + No comment provided by engineer. + new message messaggio nuovo @@ -10465,6 +11493,11 @@ time to disappear chiamata rifiutata call status + + relay + relay + member role + removed rimosso @@ -10475,6 +11508,16 @@ time to disappear ha rimosso %@ rcv group event chat item + + removed (%d attempts) + rimosso (%d tentativi) + receive error chat item + + + removed by operator + rimosso da un operatore + No comment provided by engineer. + removed contact address indirizzo di contatto rimosso @@ -10629,6 +11672,11 @@ ultimo msg ricevuto: %2$@ non protetto No comment provided by engineer. + + updated channel profile + profilo del canale aggiornato + rcv group event chat item + updated group profile ha aggiornato il profilo del gruppo @@ -10649,6 +11697,11 @@ ultimo msg ricevuto: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + via %@ + relay hostname + via contact address link via link indirizzo del contatto @@ -10724,6 +11777,11 @@ ultimo msg ricevuto: %2$@ sei un osservatore No comment provided by engineer. + + you are subscriber + sei iscritto/a + No comment provided by engineer. + you blocked %@ hai bloccato %@ @@ -10784,6 +11842,11 @@ ultimo msg ricevuto: %2$@ \~barrato~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + ⚠️ Verifica della firma fallita: %@. + owner verification + 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 ddec6c47f6..0d3a7a9088 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -185,6 +185,21 @@ %d 月 time interval + + %d relays failed + channel relay bar +channel subscriber relay bar + + + %d relays not active + channel relay bar +channel subscriber relay bar + + + %d relays removed + channel relay bar +channel subscriber relay bar + %d sec %d 秒 @@ -200,11 +215,53 @@ %d 件のスキップされたメッセージ integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d 週 time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + channel relay bar + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + channel subscriber relay bar + %lld No comment provided by engineer. @@ -214,6 +271,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld 件の連絡先が選択されました @@ -314,11 +375,19 @@ %u 件のメッセージがスキップされました。 No comment provided by engineer. + + (from owner) + chat link info line + (new) (新規) No comment provided by engineer. + + (signed) + chat link info line + (this device v%@) (このデバイス v%@) @@ -364,6 +433,10 @@ **QRスキャン / リンクの貼り付け**: 受け取ったリンクで接続する。 No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **警告**: 即時の プッシュ通知には、キーチェーンに保存されたパスフレーズが必要です。 @@ -407,6 +480,12 @@ - などなど! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -505,6 +584,10 @@ time interval その他 No comment provided by engineer. + + A link for one person to connect + No comment provided by engineer. + A new contact 新しい連絡先 @@ -628,9 +711,8 @@ swipe action アクティブな接続 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. - プロフィールにアドレスを追加し、連絡先があなたのアドレスを他の人と共有できるようにします。プロフィールの更新は連絡先に送信されます。 + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. @@ -696,6 +778,10 @@ swipe action 追加されたメッセージサーバー No comment provided by engineer. + + Adding relays will be supported later. + No comment provided by engineer. + Additional accent No comment provided by engineer. @@ -806,6 +892,14 @@ swipe action すべてのプロフィール profile dropdown + + All relays failed + No comment provided by engineer. + + + All relays removed + No comment provided by engineer. + All reports will be archived for you. No comment provided by engineer. @@ -863,6 +957,10 @@ swipe action 送信相手も永久メッセージ削除を許可する時のみに許可する。(24時間) No comment provided by engineer. + + Allow members to chat with admins. + No comment provided by engineer. + Allow message reactions only if your contact allows them. 連絡先が許可している場合にのみ、メッセージへのリアクションを許可します。 @@ -878,6 +976,10 @@ swipe action メンバーへのダイレクトメッセージを許可する。 No comment provided by engineer. + + Allow sending direct messages to subscribers. + No comment provided by engineer. + Allow sending disappearing messages. 消えるメッセージの送信を許可する。 @@ -888,6 +990,10 @@ swipe action 共有を許可 No comment provided by engineer. + + Allow subscribers to chat with admins. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) 送信済みメッセージの永久削除を許可する。(24時間) @@ -991,11 +1097,6 @@ swipe action 通話に応答 No comment provided by engineer. - - Anybody can host servers. - プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。 - No comment provided by engineer. - App build: %@ アプリのビルド: %@ @@ -1189,6 +1290,19 @@ swipe action メッセージのハッシュ値問題 No comment provided by engineer. + + Be free +in your network + No comment provided by engineer. + + + Be free in your network. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + No comment provided by engineer. + Better calls No comment provided by engineer. @@ -1266,6 +1380,10 @@ swipe action Block member? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin No comment provided by engineer. @@ -1311,6 +1429,14 @@ swipe action あなたと連絡相手が音声メッセージを送信できます。 No comment provided by engineer. + + Bottom bar + No comment provided by engineer. + + + Broadcast + compose placeholder for channel owner + 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)に感謝します! @@ -1318,7 +1444,7 @@ swipe action Business address - No comment provided by engineer. + chat link info line Business chats @@ -1337,12 +1463,6 @@ swipe action チャット プロファイル経由 (デフォルト) または [接続経由](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - No comment provided by engineer. - Call already ended! 通話は既に終了してます! @@ -1482,6 +1602,67 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + alert message +alert subtitle + + + Channel image + No comment provided by engineer. + + + Channel link + chat link info line + + + Channel preferences + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel temporarily unavailable + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + + + Channels + No comment provided by engineer. + Chat チャット @@ -1560,6 +1741,22 @@ set passcode view ユーザープロフィール No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme チャットテーマ @@ -1575,7 +1772,8 @@ set passcode view Chat with admins - chat toolbar + chat feature +chat toolbar Chat with member @@ -1590,10 +1788,22 @@ set passcode view チャット No comment provided by engineer. + + Chats with admins are prohibited. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + alert message + Chats with members No comment provided by engineer. + + Chats with members are disabled + No comment provided by engineer. + Check messages every 20 min. 20分おきにメッセージを確認する。 @@ -1603,6 +1813,14 @@ set passcode view Check messages when allowed. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. サーバのアドレスを確認してから再度試してください。 @@ -1735,8 +1953,8 @@ set passcode view ICEサーバを設定 No comment provided by engineer. - - Configure server operators + + Configure relays No comment provided by engineer. @@ -1791,7 +2009,8 @@ set passcode view Connect 接続 - server test step + relay test step +server test step Connect automatically @@ -1830,6 +2049,10 @@ This is your own one-time link! リンク経由で接続 new chat sheet title + + Connect via link or QR code + No comment provided by engineer. + Connect via one-time link ワンタイムリンクで接続 @@ -1906,7 +2129,7 @@ This is your own one-time link! Connection error (AUTH) 接続エラー (AUTH) - No comment provided by engineer. + conn error description Connection failed @@ -1956,6 +2179,10 @@ This is your own one-time link! Connections No comment provided by engineer. + + Contact address + chat link info line + Contact allows 連絡先の許可 @@ -2021,6 +2248,11 @@ This is your own one-time link! 続ける No comment provided by engineer. + + Contribute + 貢献する + No comment provided by engineer. + Conversation deleted! No comment provided by engineer. @@ -2045,12 +2277,7 @@ This is your own one-time link! Correct name to %@? - No comment provided by engineer. - - - Create - 作成 - No comment provided by engineer. + alert message Create 1-time link @@ -2098,6 +2325,14 @@ This is your own one-time link! プロフィールを作成する No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue キューの作成 @@ -2107,11 +2342,19 @@ This is your own one-time link! Create your address No comment provided by engineer. + + Create your link + No comment provided by engineer. + Create your profile プロフィールを作成する No comment provided by engineer. + + Create your public address + No comment provided by engineer. + Created No comment provided by engineer. @@ -2128,6 +2371,10 @@ This is your own one-time link! Creating archive link No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… No comment provided by engineer. @@ -2282,10 +2529,9 @@ This is your own one-time link! 配信のデバッグ No comment provided by engineer. - - Decentralized - 分散型 - No comment provided by engineer. + + Decode link + relay test step Decryption error @@ -2330,6 +2576,14 @@ swipe action Delete and notify contact No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat No comment provided by engineer. @@ -2491,6 +2745,10 @@ alert button 待ち行列を削除 server test step + + Delete relay + No comment provided by engineer. + Delete report No comment provided by engineer. @@ -2640,6 +2898,14 @@ alert button このグループではメンバー間のダイレクトメッセージが使用禁止です。 No comment provided by engineer. + + Direct messages between subscribers are prohibited. + No comment provided by engineer. + + + Disable + alert button + Disable (keep overrides) 無効にする(設定の優先を維持) @@ -2737,6 +3003,10 @@ alert button Do not send history to new members. No comment provided by engineer. + + Do not send history to new subscribers. + No comment provided by engineer. + Do not use credentials with proxy. No comment provided by engineer. @@ -2825,11 +3095,19 @@ chat item action E2E encrypted notifications. No comment provided by engineer. + + Easier to invite your friends 👋 + No comment provided by engineer. + Edit 編集する chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile グループのプロフィールを編集 @@ -2842,7 +3120,7 @@ chat item action Enable 有効 - No comment provided by engineer. + alert button Enable (keep overrides) @@ -2863,6 +3141,10 @@ chat item action TCP keep-aliveを有効にする No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? 自動メッセージ削除を有効にしますか? @@ -2872,6 +3154,10 @@ chat item action Enable camera access No comment provided by engineer. + + Enable chats with admins? + alert title + Enable disappearing messages by default. No comment provided by engineer. @@ -2890,16 +3176,15 @@ chat item action 即時通知を有効にしますか? No comment provided by engineer. + + Enable link previews? + alert title + Enable lock ロックモード No comment provided by engineer. - - Enable notifications - 通知を有効化 - No comment provided by engineer. - Enable periodic notifications? 定期的な通知を有効にしますか? @@ -2999,6 +3284,10 @@ chat item action パスコードを入力 No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. 正しいパスフレーズを入力してください。 @@ -3022,6 +3311,14 @@ chat item action 上にパスワードを入力すると表示されます! No comment provided by engineer. + + Enter profile name... + No comment provided by engineer. + + + Enter relay name… + No comment provided by engineer. + Enter server manually サーバを手動で入力 @@ -3048,7 +3345,7 @@ chat item action Error エラー - No comment provided by engineer. + conn error description Error aborting address change @@ -3073,6 +3370,10 @@ chat item action メンバー追加にエラー発生 No comment provided by engineer. + + Error adding relay + alert title + Error adding server alert title @@ -3125,6 +3426,10 @@ chat item action アドレス作成にエラー発生 No comment provided by engineer. + + Error creating channel + alert title + Error creating group グループの作成エラー @@ -3250,10 +3555,6 @@ chat item action Error opening chat No comment provided by engineer. - - Error opening group - No comment provided by engineer. - Error receiving file ファイル受信にエラー発生 @@ -3293,6 +3594,10 @@ chat item action ICEサーバ保存にエラー発生 No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list alert title @@ -3352,6 +3657,10 @@ chat item action Error setting delivery receipts! No comment provided by engineer. + + Error sharing channel + alert title + Error starting chat チャット開始にエラー発生 @@ -3426,7 +3735,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3643,7 +3953,8 @@ snd error text Fingerprint in server address does not match certificate. サーバアドレスの証明証IDが正しくないかもしれません - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3683,9 +3994,14 @@ snd error text For all moderators No comment provided by engineer. + + For anyone to reach you + No comment provided by engineer. + For chat profile %@: - servers error + servers error +servers warning For console @@ -3804,10 +4120,18 @@ Error: %2$@ GIFとステッカー No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. No comment provided by engineer. + + Get started + No comment provided by engineer. + Good afternoon! message preview @@ -3862,7 +4186,7 @@ Error: %2$@ Group link グループのリンク - No comment provided by engineer. + chat link info line Group links @@ -3970,6 +4294,10 @@ Error: %2$@ History is not sent to new members. No comment provided by engineer. + + History is not sent to new subscribers. + No comment provided by engineer. + How SimpleX works SimpleX の仕組み @@ -4063,11 +4391,6 @@ Error: %2$@ 即座に No comment provided by engineer. - - Immune to spam - スパムや悪質送信を防止 - No comment provided by engineer. - Import 読み込む @@ -4198,9 +4521,9 @@ More improvements are coming soon! 初期の役割 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) + + Install SimpleX Chat for terminal + インストール ターミナル用SimpleX Chat No comment provided by engineer. @@ -4251,7 +4574,7 @@ More improvements are coming soon! Invalid connection link 無効な接続リンク - No comment provided by engineer. + conn error description Invalid display name! @@ -4267,7 +4590,15 @@ More improvements are coming soon! Invalid name! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4302,6 +4633,10 @@ More improvements are coming soon! メンバーを招待する No comment provided by engineer. + + Invite someone privately + No comment provided by engineer. + Invite to chat No comment provided by engineer. @@ -4376,6 +4711,10 @@ More improvements are coming soon! %@ として参加 No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group グループに参加 @@ -4455,6 +4794,14 @@ This is your link for group %@! 脱退 swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat No comment provided by engineer. @@ -4477,6 +4824,10 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let someone connect to you + No comment provided by engineer. + Let's talk in SimpleX Chat SimpleXチャットで会話しよう @@ -4496,6 +4847,10 @@ This is your link for group %@! Link mobile and desktop apps! 🔗 No comment provided by engineer. + + Link signature verified. + owner verification + Linked desktop options No comment provided by engineer. @@ -4663,6 +5018,10 @@ This is your link for group %@! グループメンバーはメッセージへのリアクションを追加できます。 No comment provided by engineer. + + Members can chat with admins. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) グループのメンバーがメッセージを完全削除することができます。(24時間) @@ -4722,6 +5081,10 @@ This is your link for group %@! メッセージの下書き No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded item status text @@ -4804,6 +5167,14 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + E2EE info chat item + Messages in this chat will never be deleted. alert message @@ -4830,13 +5201,12 @@ This is your link for group %@! メッセージ、ファイル、通話は、前方秘匿性、否認可能性および侵入復元性を備えた**耐量子E2E暗号化**によって保護されます。 No comment provided by engineer. - - Migrate device + + Migrate No comment provided by engineer. - - Migrate from another device - 別の端末から移行 + + Migrate device No comment provided by engineer. @@ -4950,6 +5320,10 @@ This is your link for group %@! ネットワークとサーバ No comment provided by engineer. + + Network commitments + No comment provided by engineer. + Network connection No comment provided by engineer. @@ -4958,6 +5332,10 @@ This is your link for group %@! Network decentralization No comment provided by engineer. + + Network error + conn error description + Network issues - message expired after many attempts to send it. snd error text @@ -4970,6 +5348,11 @@ This is your link for group %@! Network operator No comment provided by engineer. + + Network routers cannot know +who talks to whom + No comment provided by engineer. + Network settings ネットワーク設定 @@ -4984,6 +5367,10 @@ This is your link for group %@! New token status text + + New 1-time link + No comment provided by engineer. + New Passcode 新しいパスコード @@ -5005,6 +5392,10 @@ This is your link for group %@! New chat experience 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request 新しい繋がりのリクエスト @@ -5070,11 +5461,28 @@ This is your link for group %@! いいえ No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + No comment provided by engineer. + + + No active relays + No comment provided by engineer. + No app password アプリのパスワードはありません Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats No comment provided by engineer. @@ -5201,11 +5609,22 @@ This is your link for group %@! No unread chats No comment provided by engineer. - - No user identifiers. - 世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。 + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. + + Non-profit governance + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + No comment provided by engineer. + + + Not all relays connected + alert title + Not compatible! No comment provided by engineer. @@ -5255,7 +5674,7 @@ This is your link for group %@! OK - No comment provided by engineer. + alert button Off @@ -5274,11 +5693,19 @@ new chat action 古いデータベース No comment provided by engineer. + + On your phone, not on servers. + No comment provided by engineer. + One-time invitation link 使い捨ての招待リンク No comment provided by engineer. + + One-time link + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5298,6 +5725,10 @@ VPN を有効にする必要があります。 オニオンのホストが使われません。 No comment provided by engineer. + + Only channel owners can change channel preferences. + No comment provided by engineer. + Only chat owners can change preferences. No comment provided by engineer. @@ -5395,7 +5826,8 @@ VPN を有効にする必要があります。 Open 開く - alert action + alert action +alert button Open Settings @@ -5406,6 +5838,10 @@ VPN を有効にする必要があります。 Open changes No comment provided by engineer. + + Open channel + new chat action + Open chat チャットを開く @@ -5424,6 +5860,10 @@ VPN を有効にする必要があります。 Open conditions No comment provided by engineer. + + Open external link? + alert title + Open full link alert action @@ -5440,6 +5880,10 @@ VPN を有効にする必要があります。 Open migration to another device authentication reason + + Open new channel + new chat action + Open new chat new chat action @@ -5476,6 +5920,13 @@ VPN を有効にする必要があります。 Operator server alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + No comment provided by engineer. + Or import archive file No comment provided by engineer. @@ -5492,6 +5943,10 @@ VPN を有効にする必要があります。 Or securely share this file link No comment provided by engineer. + + Or show QR in person or via video call. + No comment provided by engineer. + Or show this code No comment provided by engineer. @@ -5500,6 +5955,10 @@ VPN を有効にする必要があります。 Or to share privately No comment provided by engineer. + + Or use this QR - print or show online. + No comment provided by engineer. + Organize chats into lists No comment provided by engineer. @@ -5513,6 +5972,18 @@ VPN を有効にする必要があります。 %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + + + Ownership: you can run your own relays. + No comment provided by engineer. + PING count PING回数 @@ -5566,6 +6037,10 @@ VPN を有効にする必要があります。 画像の貼り付け No comment provided by engineer. + + Paste link / Scan + No comment provided by engineer. + Paste link to connect! No comment provided by engineer. @@ -5704,6 +6179,14 @@ Error: %@ 添付を含めて、下書きを保存する。 No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address プレセットサーバのアドレス @@ -5735,13 +6218,12 @@ Error: %@ Privacy policy and conditions of use. No comment provided by engineer. - - Privacy redefined - プライバシーの基準を新境地に + + Privacy: for owners and subscribers. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. + + Private and secure messaging. No comment provided by engineer. @@ -5778,6 +6260,10 @@ Error: %@ Private routing timeout alert title + + Proceed + alert action + Profile and server connections プロフィールとサーバ接続 @@ -5801,9 +6287,8 @@ Error: %@ Profile theme No comment provided by engineer. - - Profile update will be sent to your contacts. - 連絡先にプロフィール更新のお知らせが届きます。 + + Profile update will be sent to your SimpleX contacts. alert message @@ -5811,6 +6296,10 @@ Error: %@ 音声/ビデオ通話を禁止する 。 No comment provided by engineer. + + Prohibit chats with admins. + No comment provided by engineer. + Prohibit irreversible message deletion. メッセージの完全削除を使用禁止にする。 @@ -5839,6 +6328,10 @@ Error: %@ メンバー間のダイレクトメッセージを使用禁止にする。 No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + No comment provided by engineer. + Prohibit sending disappearing messages. 消えるメッセージを使用禁止にする。 @@ -5899,6 +6392,10 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. + + Public channels - speak freely 🚀 + No comment provided by engineer. + Push notifications プッシュ通知 @@ -5936,23 +6433,14 @@ Enable in *Network & servers* settings. 続きを読む No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + 詳しくはユーザーガイドをご覧ください。 No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - 詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)をご覧ください。 - 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)をご覧ください。 + + Read more in our GitHub repository. + 詳しくはGitHubリポジトリをご覧ください。 No comment provided by engineer. @@ -6096,6 +6584,26 @@ swipe action Reject member? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + + + Relay results: + alert message + Relay server is only used if necessary. Another party can observe your IP address. 中継サーバーは必要な場合にのみ使用されます。 別の当事者があなたの IP アドレスを監視できます。 @@ -6106,6 +6614,14 @@ swipe action リレー サーバーは IP アドレスを保護しますが、通話時間は監視されます。 No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + + + Reliability: many relays per channel. + No comment provided by engineer. + Remove 削除 @@ -6142,6 +6658,14 @@ swipe action キーチェーンからパスフレーズを削除しますか? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. No comment provided by engineer. @@ -6350,6 +6874,10 @@ swipe action SOCKS proxy No comment provided by engineer. + + Safe web links + No comment provided by engineer. + Safely receive files No comment provided by engineer. @@ -6373,6 +6901,10 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + alert button + Save admission settings? alert title @@ -6387,6 +6919,10 @@ chat item action 保存して、グループのメンバーにに知らせる No comment provided by engineer. + + Save and notify subscribers + No comment provided by engineer. + Save and reconnect No comment provided by engineer. @@ -6396,6 +6932,14 @@ chat item action グループプロファイルの保存と更新 No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile グループプロフィールの保存 @@ -6557,6 +7101,10 @@ chat item action セキュリティコード No comment provided by engineer. + + Security: owners hold channel keys. + No comment provided by engineer. + Select 選択 @@ -6674,6 +7222,10 @@ chat item action Send request without message No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. ギャラリーまたはカスタム キーボードから送信します。 @@ -6683,6 +7235,10 @@ chat item action Send up to 100 last messages to new members. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + No comment provided by engineer. + Send your private feedback to groups. No comment provided by engineer. @@ -6697,6 +7253,10 @@ chat item action 送信元が繋がりリクエストを削除したかもしれません。 No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. No comment provided by engineer. @@ -6803,6 +7363,10 @@ chat item action Server protocol changed. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. キューを作成するにはサーバーの認証が必要です。パスワードを確認してください @@ -6920,6 +7484,14 @@ chat item action Settings were changed. alert message + + Setup notifications + No comment provided by engineer. + + + Setup routers + No comment provided by engineer. + Shape profile images No comment provided by engineer. @@ -6952,11 +7524,14 @@ chat item action Share address publicly No comment provided by engineer. - - Share address with contacts? - アドレスを連絡先と共有しますか? + + Share address with SimpleX contacts? alert title + + Share channel + No comment provided by engineer. + Share from other apps. No comment provided by engineer. @@ -6978,6 +7553,10 @@ chat item action Share profile No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. @@ -6986,9 +7565,12 @@ chat item action Share to SimpleX No comment provided by engineer. - - Share with contacts - 連絡先と共有する + + Share via chat + No comment provided by engineer. + + + Share with SimpleX contacts No comment provided by engineer. @@ -7144,8 +7726,8 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7212,6 +7794,11 @@ report reason Square, circle, or anything in between. No comment provided by engineer. + + Star on GitHub + GitHub でスターを付ける + No comment provided by engineer. + Start chat チャットを開始する @@ -7304,6 +7891,63 @@ report reason Subscribed No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber reports + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers can add message reactions. + No comment provided by engineer. + + + Subscribers can chat with admins. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + No comment provided by engineer. + + + Subscribers can send direct messages. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + No comment provided by engineer. + + + Subscribers can send files and media. + No comment provided by engineer. + + + Subscribers can send voice messages. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors No comment provided by engineer. @@ -7376,6 +8020,10 @@ report reason 写真を撮影 No comment provided by engineer. + + Talk to someone + No comment provided by engineer. + Tap Connect to chat No comment provided by engineer. @@ -7388,8 +8036,8 @@ report reason Tap Connect to use bot No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. + + Tap Join channel No comment provided by engineer. @@ -7420,6 +8068,10 @@ report reason タップしてシークレットモードで参加 No comment provided by engineer. + + Tap to open + No comment provided by engineer. + Tap to paste link No comment provided by engineer. @@ -7435,12 +8087,17 @@ report reason Test failed at step %@. テストはステップ %@ で失敗しました。 - server test failure + relay test failure +server test failure Test notifications No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server テストサーバ @@ -7491,6 +8148,10 @@ It can happen because of some bug or when the connection is compromised.The app protects your privacy by using different operators in each conversation. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -7504,6 +8165,10 @@ It can happen because of some bug or when the connection is compromised.The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. No comment provided by engineer. @@ -7528,9 +8193,9 @@ It can happen because of some bug or when the connection is compromised.暗号化は機能しており、新しい暗号化への同意は必要ありません。接続エラーが発生する可能性があります! No comment provided by engineer. - - The future of messaging - 次世代のプライバシー・メッセンジャー + + The first network where you own +your contacts and groups. No comment provided by engineer. @@ -7565,6 +8230,10 @@ It can happen because of some bug or when the connection is compromised.古いデータベースは移行時に削除されなかったので、削除することができます。 No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + No comment provided by engineer. + The same conditions will apply to operator **%@**. No comment provided by engineer. @@ -7604,6 +8273,14 @@ It can happen because of some bug or when the connection is compromised.Themes No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + No comment provided by engineer. + These conditions will also apply for: **%@**. No comment provided by engineer. @@ -7662,6 +8339,14 @@ It can happen because of some bug or when the connection is compromised.このグループはもう存在しません。 No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -7705,6 +8390,10 @@ It can happen because of some bug or when the connection is compromised.To hide unwanted messages. No comment provided by engineer. + + To make SimpleX Network last. + No comment provided by engineer. + To make a new connection 新規に接続する場合 @@ -7783,10 +8472,6 @@ You will be prompted to complete authentication before this feature is enabled.< エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。 No comment provided by engineer. - - Toggle chat list: - No comment provided by engineer. - Toggle incognito when connecting. No comment provided by engineer. @@ -7799,6 +8484,10 @@ You will be prompted to complete authentication before this feature is enabled.< Toolbar opacity No comment provided by engineer. + + Top bar + No comment provided by engineer. + Total No comment provided by engineer. @@ -7855,6 +8544,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -7950,12 +8643,16 @@ To connect, please ask your contact to create another connection link and check Unsupported connection link - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + No comment provided by engineer. + Update 更新 @@ -8064,11 +8761,6 @@ To connect, please ask your contact to create another connection link and check Use TCP port 443 for preset servers only. No comment provided by engineer. - - Use chat - チャット - No comment provided by engineer. - Use current profile 現在のプロファイルを使用する @@ -8082,6 +8774,10 @@ To connect, please ask your contact to create another connection link and check Use for messages No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections 新しい接続に使う @@ -8117,6 +8813,10 @@ To connect, please ask your contact to create another connection link and check Use private routing with unknown servers. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server サーバを使う @@ -8134,6 +8834,10 @@ To connect, please ask your contact to create another connection link and check Use the app with one hand. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + No comment provided by engineer. + Use web port No comment provided by engineer. @@ -8151,6 +8855,10 @@ To connect, please ask your contact to create another connection link and check SimpleX チャット サーバーを使用する。 No comment provided by engineer. + + Verify + relay test step + Verify code with desktop No comment provided by engineer. @@ -8260,6 +8968,18 @@ To connect, please ask your contact to create another connection link and check 音声メッセージ… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + + + Waiting for channel owner to add relays. + No comment provided by engineer. + Waiting for desktop... No comment provided by engineer. @@ -8296,6 +9016,10 @@ To connect, please ask your contact to create another connection link and check 警告: 一部のデータが失われる可能性があります! No comment provided by engineer. + + We made connecting simpler for new users. + No comment provided by engineer. + WebRTC ICE servers WebRTC ICEサーバ @@ -8342,6 +9066,10 @@ To connect, please ask your contact to create another connection link and check 連絡相手にシークレットモードのプロフィールを共有すると、その連絡相手に招待されたグループでも同じプロフィールが使われます。 No comment provided by engineer. + + Why SimpleX is built. + No comment provided by engineer. + WiFi No comment provided by engineer. @@ -8527,6 +9255,10 @@ Repeat join request? 設定からロック画面の通知プレビューを設定できます。 No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + 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コードを共有できます。誰でもグループに参加できます。後で削除しても、グループのメンバーがそのままのこります。 @@ -8569,16 +9301,21 @@ Repeat join request? メッセージを送信できませんでした! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. 確認できませんでした。 もう一度お試しください。 No comment provided by engineer. - - You decide who can connect. - あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。 - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -8640,6 +9377,10 @@ Repeat connection request? You should receive notifications. token info + + You were born without an account + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. No comment provided by engineer. @@ -8673,6 +9414,10 @@ Repeat connection request? ミュートされたプロフィールがアクティブな場合でも、そのプロフィールからの通話や通知は引き続き受信します。 No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. No comment provided by engineer. @@ -8716,6 +9461,10 @@ Repeat connection request? あなたの通話 No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database あなたのチャットデータベース @@ -8762,6 +9511,10 @@ Repeat connection request? 連絡先は接続されたままになります。 No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + No comment provided by engineer. + Your credentials may be sent unencrypted. No comment provided by engineer. @@ -8780,6 +9533,10 @@ Repeat connection request? Your group No comment provided by engineer. + + Your network + No comment provided by engineer. + Your preferences あなたの設定 @@ -8794,6 +9551,11 @@ Repeat connection request? Your profile No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. あなたのプロファイル **%@** が共有されます。 @@ -8813,11 +9575,23 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. alert message + + Your public address + No comment provided by engineer. + Your random profile あなたのランダム・プロフィール No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address あなたのサーバアドレス @@ -8832,21 +9606,11 @@ Repeat connection request? あなたの設定 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. - \_italic_ \_斜体_ @@ -8862,6 +9626,10 @@ Repeat connection request? 上で選んでください: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ rcv group event chat item @@ -8879,6 +9647,10 @@ Repeat connection request? accepted you rcv group event chat item + + active + No comment provided by engineer. + admin 管理者 @@ -8979,6 +9751,10 @@ marked deleted chat item preview text 発信中… call status + + can't broadcast + No comment provided by engineer. + can't send messages No comment provided by engineer. @@ -9013,6 +9789,14 @@ marked deleted chat item preview text アドレスを変更しています… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored 色付き @@ -9153,6 +9937,10 @@ pref value 削除完了 deleted chat item + + deleted channel + rcv group event chat item + deleted contact rcv direct event chat item @@ -9261,6 +10049,10 @@ pref value エラー No comment provided by engineer. + + error: %@ + receive error chat item + expired No comment provided by engineer. @@ -9384,6 +10176,10 @@ pref value 脱退 rcv group event chat item + + link + No comment provided by engineer. + marked deleted 削除済みとマーク @@ -9450,6 +10246,10 @@ pref value 一度も delete after time + + new + No comment provided by engineer. + new message 新しいメッセージ @@ -9563,6 +10363,10 @@ time to disappear 拒否した通話 call status + + relay + member role + removed 除名されました @@ -9573,6 +10377,14 @@ time to disappear %@ を除名されました rcv group event chat item + + removed (%d attempts) + receive error chat item + + + removed by operator + No comment provided by engineer. + removed contact address profile update event chat item @@ -9704,6 +10516,10 @@ last received msg: %2$@ unprotected No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile グループプロフィールを更新しました @@ -9722,6 +10538,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link 連絡先アドレスリンク経由 @@ -9793,6 +10613,10 @@ last received msg: %2$@ あなたはオブザーバーです No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ snd group event chat item @@ -9851,6 +10675,10 @@ last received msg: %2$@ \~取り消し線~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + owner verification + 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 e12cb0a483..3bf4a6f197 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -185,6 +185,21 @@ %d maanden time interval + + %d relays failed + channel relay bar +channel subscriber relay bar + + + %d relays not active + channel relay bar +channel subscriber relay bar + + + %d relays removed + channel relay bar +channel subscriber relay bar + %d sec %d sec @@ -200,11 +215,53 @@ %d overgeslagen bericht(en) integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d weken time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + channel relay bar + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + channel subscriber relay bar + %lld %lld @@ -215,6 +272,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld contact(en) geselecteerd @@ -315,11 +376,19 @@ %u berichten zijn overgeslagen. No comment provided by engineer. + + (from owner) + chat link info line + (new) (nieuw) No comment provided by engineer. + + (signed) + chat link info line + (this device v%@) (dit apparaat v%@) @@ -365,6 +434,10 @@ **Link scannen/plakken**: om verbinding te maken via een link die u hebt ontvangen. No comment provided by engineer. + + **Test relay** to retrieve its name. + 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. @@ -408,6 +481,12 @@ - en meer! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -506,6 +585,10 @@ time interval Nog een paar dingen No comment provided by engineer. + + A link for one person to connect + No comment provided by engineer. + A new contact Een nieuw contact @@ -631,9 +714,8 @@ swipe action Actieve verbindingen 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. - Voeg een adres toe aan uw profiel, zodat uw contacten het met andere mensen kunnen delen. Profiel update wordt naar uw contacten verzonden. + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. @@ -700,6 +782,10 @@ swipe action Berichtservers toegevoegd No comment provided by engineer. + + Adding relays will be supported later. + No comment provided by engineer. + Additional accent Extra accent @@ -819,6 +905,14 @@ swipe action Alle profielen profile dropdown + + All relays failed + No comment provided by engineer. + + + All relays removed + No comment provided by engineer. + All reports will be archived for you. Alle rapporten worden voor u gearchiveerd. @@ -878,6 +972,10 @@ swipe action Sta het definitief verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur) No comment provided by engineer. + + Allow members to chat with admins. + No comment provided by engineer. + Allow message reactions only if your contact allows them. Sta bericht reacties alleen toe als uw contact dit toestaat. @@ -893,6 +991,10 @@ swipe action Sta het verzenden van directe berichten naar leden toe. No comment provided by engineer. + + Allow sending direct messages to subscribers. + No comment provided by engineer. + Allow sending disappearing messages. Toestaan dat verdwijnende berichten worden verzonden. @@ -903,6 +1005,10 @@ swipe action Delen toestaan No comment provided by engineer. + + Allow subscribers to chat with admins. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Sta toe om verzonden berichten definitief te verwijderen. (24 uur) @@ -1007,11 +1113,6 @@ swipe action Beantwoord oproep No comment provided by engineer. - - Anybody can host servers. - Iedereen kan servers hosten. - No comment provided by engineer. - App build: %@ App build: %@ @@ -1216,6 +1317,19 @@ swipe action Onjuiste bericht hash No comment provided by engineer. + + Be free +in your network + No comment provided by engineer. + + + Be free in your network. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + No comment provided by engineer. + Better calls Betere gesprekken @@ -1309,6 +1423,10 @@ swipe action Lid blokkeren? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Geblokkeerd door beheerder @@ -1357,6 +1475,14 @@ swipe action Zowel jij als je contact kunnen spraak berichten verzenden. No comment provided by engineer. + + Bottom bar + No comment provided by engineer. + + + Broadcast + compose placeholder for channel owner + 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)! @@ -1365,7 +1491,7 @@ swipe action Business address Zakelijk adres - No comment provided by engineer. + chat link info line Business chats @@ -1386,15 +1512,6 @@ swipe action Via chatprofiel (standaard) of [via verbinding](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - Door SimpleX Chat te gebruiken, gaat u ermee akkoord: -- alleen legale content te versturen in openbare groepen. -- andere gebruikers te respecteren – geen spam. - No comment provided by engineer. - Call already ended! Oproep al beëindigd! @@ -1542,6 +1659,67 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + alert message +alert subtitle + + + Channel image + No comment provided by engineer. + + + Channel link + chat link info line + + + Channel preferences + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel temporarily unavailable + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + + + Channels + No comment provided by engineer. + Chat Chat @@ -1627,6 +1805,22 @@ set passcode view Gebruikers profiel No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Chat thema @@ -1645,7 +1839,8 @@ set passcode view Chat with admins Chat met beheerders - chat toolbar + chat feature +chat toolbar Chat with member @@ -1661,11 +1856,23 @@ set passcode view Chats No comment provided by engineer. + + Chats with admins are prohibited. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + alert message + Chats with members Chats met leden No comment provided by engineer. + + Chats with members are disabled + No comment provided by engineer. + Check messages every 20 min. Controleer uw berichten elke 20 minuten. @@ -1676,6 +1883,14 @@ set passcode view Controleer berichten indien toegestaan. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Controleer het server adres en probeer het opnieuw. @@ -1821,9 +2036,8 @@ set passcode view ICE servers configureren No comment provided by engineer. - - Configure server operators - Serveroperators configureren + + Configure relays No comment provided by engineer. @@ -1884,7 +2098,8 @@ set passcode view Connect Verbind - server test step + relay test step +server test step Connect automatically @@ -1929,6 +2144,10 @@ Dit is uw eigen eenmalige link! Maak verbinding via link new chat sheet title + + Connect via link or QR code + No comment provided by engineer. + Connect via one-time link Verbinden via een eenmalige link? @@ -2007,7 +2226,7 @@ Dit is uw eigen eenmalige link! Connection error (AUTH) Verbindingsfout (AUTH) - No comment provided by engineer. + conn error description Connection failed @@ -2065,6 +2284,10 @@ Dit is uw eigen eenmalige link! Verbindingen No comment provided by engineer. + + Contact address + chat link info line + Contact allows Contact maakt het mogelijk @@ -2134,6 +2357,11 @@ Dit is uw eigen eenmalige link! Doorgaan No comment provided by engineer. + + Contribute + Bijdragen + No comment provided by engineer. + Conversation deleted! Gesprek verwijderd! @@ -2162,12 +2390,7 @@ Dit is uw eigen eenmalige link! Correct name to %@? Juiste naam voor %@? - No comment provided by engineer. - - - Create - Maak - No comment provided by engineer. + alert message Create 1-time link @@ -2219,6 +2442,14 @@ Dit is uw eigen eenmalige link! Maak een profiel aan No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Maak een wachtrij @@ -2228,11 +2459,19 @@ Dit is uw eigen eenmalige link! Create your address No comment provided by engineer. + + Create your link + No comment provided by engineer. + Create your profile Maak je profiel aan No comment provided by engineer. + + Create your public address + No comment provided by engineer. + Created Gemaakt @@ -2253,6 +2492,10 @@ Dit is uw eigen eenmalige link! Archief link maken No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Link maken… @@ -2411,10 +2654,9 @@ Dit is uw eigen eenmalige link! Foutopsporing bezorging No comment provided by engineer. - - Decentralized - Gedecentraliseerd - No comment provided by engineer. + + Decode link + relay test step Decryption error @@ -2462,6 +2704,14 @@ swipe action Verwijderen en contact op de hoogte stellen No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Chat verwijderen @@ -2631,6 +2881,10 @@ alert button Wachtrij verwijderen server test step + + Delete relay + No comment provided by engineer. + Delete report Rapport verwijderen @@ -2794,6 +3048,14 @@ alert button Directe berichten tussen leden zijn niet toegestaan. No comment provided by engineer. + + Direct messages between subscribers are prohibited. + No comment provided by engineer. + + + Disable + alert button + Disable (keep overrides) Uitschakelen (overschrijvingen behouden) @@ -2899,6 +3161,10 @@ alert button Stuur geen geschiedenis naar nieuwe leden. No comment provided by engineer. + + Do not send history to new subscribers. + No comment provided by engineer. + Do not use credentials with proxy. Gebruik geen inloggegevens met proxy. @@ -3000,11 +3266,19 @@ chat item action E2E versleutelde meldingen. No comment provided by engineer. + + Easier to invite your friends 👋 + No comment provided by engineer. + Edit Bewerk chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Groep profiel bewerken @@ -3017,7 +3291,7 @@ chat item action Enable Inschakelen - No comment provided by engineer. + alert button Enable (keep overrides) @@ -3039,6 +3313,10 @@ chat item action Schakel TCP keep-alive in No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Automatisch verwijderen van berichten aanzetten? @@ -3049,6 +3327,10 @@ chat item action Schakel cameratoegang in No comment provided by engineer. + + Enable chats with admins? + alert title + Enable disappearing messages by default. No comment provided by engineer. @@ -3068,16 +3350,15 @@ chat item action Onmiddellijke meldingen inschakelen? No comment provided by engineer. + + Enable link previews? + alert title + Enable lock Vergrendeling inschakelen No comment provided by engineer. - - Enable notifications - Meldingen aanzetten - No comment provided by engineer. - Enable periodic notifications? Periodieke meldingen inschakelen? @@ -3183,6 +3464,10 @@ chat item action Voer toegangscode in No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Voer het juiste wachtwoord in. @@ -3208,6 +3493,14 @@ chat item action Voer hier boven het wachtwoord in om weer te geven! No comment provided by engineer. + + Enter profile name... + No comment provided by engineer. + + + Enter relay name… + No comment provided by engineer. + Enter server manually Voer de server handmatig in @@ -3236,7 +3529,7 @@ chat item action Error Fout - No comment provided by engineer. + conn error description Error aborting address change @@ -3263,6 +3556,10 @@ chat item action Fout bij het toevoegen van leden No comment provided by engineer. + + Error adding relay + alert title + Error adding server Fout bij toevoegen server @@ -3320,6 +3617,10 @@ chat item action Fout bij aanmaken van adres No comment provided by engineer. + + Error creating channel + alert title + Error creating group Fout bij maken van groep @@ -3455,10 +3756,6 @@ chat item action Fout bij het openen van de chat No comment provided by engineer. - - Error opening group - No comment provided by engineer. - Error receiving file Fout bij ontvangen van bestand @@ -3503,6 +3800,10 @@ chat item action Fout bij opslaan van ICE servers No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Fout bij het opslaan van chatlijst @@ -3567,6 +3868,10 @@ chat item action Fout bij het instellen van ontvangst bevestiging! No comment provided by engineer. + + Error sharing channel + alert title + Error starting chat Fout bij het starten van de chat @@ -3646,7 +3951,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3886,7 +4192,8 @@ snd error text Fingerprint in server address does not match certificate. Mogelijk is de certificaat vingerafdruk in het server adres onjuist - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3927,10 +4234,15 @@ snd error text Voor alle moderators No comment provided by engineer. + + For anyone to reach you + No comment provided by engineer. + For chat profile %@: Voor chatprofiel %@: - servers error + servers error +servers warning For console @@ -4071,11 +4383,19 @@ Fout: %2$@ GIF's en stickers No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. Ontvang een melding als u vermeld wordt. No comment provided by engineer. + + Get started + No comment provided by engineer. + Good afternoon! Goedemiddag! @@ -4134,7 +4454,7 @@ Fout: %2$@ Group link Groep link - No comment provided by engineer. + chat link info line Group links @@ -4245,6 +4565,10 @@ Fout: %2$@ Geschiedenis wordt niet naar nieuwe leden gestuurd. No comment provided by engineer. + + History is not sent to new subscribers. + No comment provided by engineer. + How SimpleX works Hoe SimpleX werkt @@ -4343,11 +4667,6 @@ Fout: %2$@ Onmiddellijk No comment provided by engineer. - - Immune to spam - Immuun voor spam en misbruik - No comment provided by engineer. - Import Importeren @@ -4490,9 +4809,9 @@ Binnenkort meer verbeteringen! Initiële rol No comment provided by engineer. - - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - Installeer [SimpleX Chat voor terminal](https://github.com/simplex-chat/simplex-chat) + + Install SimpleX Chat for terminal + Installeer SimpleX Chat voor terminal No comment provided by engineer. @@ -4550,7 +4869,7 @@ Binnenkort meer verbeteringen! Invalid connection link Ongeldige verbinding link - No comment provided by engineer. + conn error description Invalid display name! @@ -4570,7 +4889,15 @@ Binnenkort meer verbeteringen! Invalid name! Ongeldige naam! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4606,6 +4933,10 @@ Binnenkort meer verbeteringen! Nodig leden uit No comment provided by engineer. + + Invite someone privately + No comment provided by engineer. + Invite to chat Uitnodigen voor een chat @@ -4682,6 +5013,10 @@ Binnenkort meer verbeteringen! deelnemen als %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Word lid van groep @@ -4768,6 +5103,14 @@ Dit is jouw link voor groep %@! Verlaten swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Chat verlaten @@ -4792,6 +5135,10 @@ Dit is jouw link voor groep %@! Less traffic on mobile networks. No comment provided by engineer. + + Let someone connect to you + No comment provided by engineer. + Let's talk in SimpleX Chat Laten we praten in SimpleX Chat @@ -4812,6 +5159,10 @@ Dit is jouw link voor groep %@! Koppel mobiele en desktop-apps! 🔗 No comment provided by engineer. + + Link signature verified. + owner verification + Linked desktop options Gekoppelde desktop opties @@ -4992,6 +5343,10 @@ Dit is jouw link voor groep %@! Groepsleden kunnen bericht reacties toevoegen. No comment provided by engineer. + + Members can chat with admins. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur) @@ -5057,6 +5412,10 @@ Dit is jouw link voor groep %@! Concept bericht No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Bericht doorgestuurd @@ -5150,6 +5509,14 @@ Dit is jouw link voor groep %@! Berichten van %@ worden getoond! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + E2EE info chat item + Messages in this chat will never be deleted. Berichten in deze chat zullen nooit worden verwijderd. @@ -5180,16 +5547,15 @@ Dit is jouw link voor groep %@! Berichten, bestanden en oproepen worden beschermd door **kwantumbestendige e2e encryptie** met perfecte voorwaartse geheimhouding, afwijzing en inbraakherstel. No comment provided by engineer. + + Migrate + No comment provided by engineer. + Migrate device Apparaat migreren No comment provided by engineer. - - Migrate from another device - Migreer vanaf een ander apparaat - No comment provided by engineer. - Migrate here Migreer hierheen @@ -5310,6 +5676,10 @@ Dit is jouw link voor groep %@! Netwerk & servers No comment provided by engineer. + + Network commitments + No comment provided by engineer. + Network connection Netwerkverbinding @@ -5320,6 +5690,10 @@ Dit is jouw link voor groep %@! Netwerk decentralisatie No comment provided by engineer. + + Network error + conn error description + Network issues - message expired after many attempts to send it. Netwerkproblemen - bericht is verlopen na vele pogingen om het te verzenden. @@ -5335,6 +5709,11 @@ Dit is jouw link voor groep %@! Netwerkbeheerder No comment provided by engineer. + + Network routers cannot know +who talks to whom + No comment provided by engineer. + Network settings Netwerk instellingen @@ -5350,6 +5729,10 @@ Dit is jouw link voor groep %@! Nieuw token status text + + New 1-time link + No comment provided by engineer. + New Passcode Nieuwe toegangscode @@ -5375,6 +5758,10 @@ Dit is jouw link voor groep %@! Nieuwe chatervaring 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Nieuw contactverzoek @@ -5444,11 +5831,28 @@ Dit is jouw link voor groep %@! Nee No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + No comment provided by engineer. + + + No active relays + No comment provided by engineer. + No app password Geen app wachtwoord Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats Geen chats @@ -5593,11 +5997,22 @@ Dit is jouw link voor groep %@! Geen ongelezen chats No comment provided by engineer. - - No user identifiers. - Geen gebruikers-ID's. + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. + + Non-profit governance + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + No comment provided by engineer. + + + Not all relays connected + alert title + Not compatible! Niet compatibel! @@ -5655,7 +6070,7 @@ Dit is jouw link voor groep %@! OK OK - No comment provided by engineer. + alert button Off @@ -5674,11 +6089,19 @@ new chat action Oude database No comment provided by engineer. + + On your phone, not on servers. + No comment provided by engineer. + One-time invitation link Eenmalige uitnodiging link No comment provided by engineer. + + One-time link + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5698,6 +6121,10 @@ Vereist het inschakelen van VPN. Onion hosts worden niet gebruikt. No comment provided by engineer. + + Only channel owners can change channel preferences. + No comment provided by engineer. + Only chat owners can change preferences. Alleen chateigenaren kunnen voorkeuren wijzigen. @@ -5799,7 +6226,8 @@ Vereist het inschakelen van VPN. Open Open - alert action + alert action +alert button Open Settings @@ -5811,6 +6239,10 @@ Vereist het inschakelen van VPN. Wijzigingen openen No comment provided by engineer. + + Open channel + new chat action + Open chat Chat openen @@ -5830,6 +6262,10 @@ Vereist het inschakelen van VPN. Open voorwaarden No comment provided by engineer. + + Open external link? + alert title + Open full link alert action @@ -5849,6 +6285,10 @@ Vereist het inschakelen van VPN. Open de migratie naar een ander apparaat authentication reason + + Open new channel + new chat action + Open new chat new chat action @@ -5888,6 +6328,13 @@ Vereist het inschakelen van VPN. Operatorserver alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + No comment provided by engineer. + Or import archive file Of importeer archiefbestand @@ -5908,6 +6355,10 @@ Vereist het inschakelen van VPN. Of deel deze bestands link veilig No comment provided by engineer. + + Or show QR in person or via video call. + No comment provided by engineer. + Or show this code Of laat deze code zien @@ -5918,6 +6369,10 @@ Vereist het inschakelen van VPN. Of om privé te delen No comment provided by engineer. + + Or use this QR - print or show online. + No comment provided by engineer. + Organize chats into lists Organiseer chats in lijsten @@ -5935,6 +6390,18 @@ Vereist het inschakelen van VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + + + Ownership: you can run your own relays. + No comment provided by engineer. + PING count PING count @@ -5990,6 +6457,10 @@ Vereist het inschakelen van VPN. Afbeelding plakken No comment provided by engineer. + + Paste link / Scan + No comment provided by engineer. + Paste link to connect! Plak een link om te verbinden! @@ -6144,6 +6615,14 @@ Fout: %@ Bewaar het laatste berichtconcept, met bijlagen. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Vooraf ingesteld server adres @@ -6179,14 +6658,12 @@ Fout: %@ Privacybeleid en gebruiksvoorwaarden. No comment provided by engineer. - - Privacy redefined - Privacy opnieuw gedefinieerd + + Privacy: for owners and subscribers. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. - Privéchats, groepen en uw contacten zijn niet toegankelijk voor serverbeheerders. + + Private and secure messaging. No comment provided by engineer. @@ -6228,6 +6705,10 @@ Fout: %@ Private routing timeout alert title + + Proceed + alert action + Profile and server connections Profiel- en serververbindingen @@ -6253,9 +6734,8 @@ Fout: %@ Profiel thema No comment provided by engineer. - - Profile update will be sent to your contacts. - Profiel update wordt naar uw contacten verzonden. + + Profile update will be sent to your SimpleX contacts. alert message @@ -6263,6 +6743,10 @@ Fout: %@ Audio/video gesprekken verbieden. No comment provided by engineer. + + Prohibit chats with admins. + No comment provided by engineer. + Prohibit irreversible message deletion. Verbied het definitief verwijderen van berichten. @@ -6293,6 +6777,10 @@ Fout: %@ Verbied het sturen van directe berichten naar leden. No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + No comment provided by engineer. + Prohibit sending disappearing messages. Verbied het verzenden van verdwijnende berichten. @@ -6359,6 +6847,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Proxy vereist wachtwoord No comment provided by engineer. + + Public channels - speak freely 🚀 + No comment provided by engineer. + Push notifications Push meldingen @@ -6399,24 +6891,14 @@ Schakel dit in in *Netwerk en servers*-instellingen. Lees meer No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + Lees meer in de Gebruikershandleiding. No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/app-settings.html#uw-simplex-contactadres). - No comment provided by engineer. - - - Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - Lees meer in de [Gebruikershandleiding](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). - Lees meer in onze [GitHub-repository](https://github.com/simplex-chat/simplex-chat#readme). + + Read more in our GitHub repository. + Lees meer in onze GitHub-repository. No comment provided by engineer. @@ -6576,6 +7058,26 @@ swipe action Lid afwijzen? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + + + Relay results: + alert message + Relay server is only used if necessary. Another party can observe your IP address. Relay server wordt alleen gebruikt als dat nodig is. Een andere partij kan uw IP-adres zien. @@ -6586,6 +7088,14 @@ swipe action Relay server beschermt uw IP-adres, maar kan de duur van het gesprek observeren. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + + + Reliability: many relays per channel. + No comment provided by engineer. + Remove Verwijderen @@ -6624,6 +7134,14 @@ swipe action Wachtwoord van de keychain verwijderen? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. No comment provided by engineer. @@ -6857,6 +7375,10 @@ swipe action SOCKS proxy No comment provided by engineer. + + Safe web links + No comment provided by engineer. + Safely receive files Veilig bestanden ontvangen @@ -6882,6 +7404,10 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + alert button + Save admission settings? Toegangsinstellingen opslaan? @@ -6897,6 +7423,10 @@ chat item action Opslaan en groep leden melden No comment provided by engineer. + + Save and notify subscribers + No comment provided by engineer. + Save and reconnect Opslaan en opnieuw verbinden @@ -6907,6 +7437,14 @@ chat item action Groep profiel opslaan en bijwerken No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Groep profiel opslaan @@ -7081,6 +7619,10 @@ chat item action Beveiligingscode No comment provided by engineer. + + Security: owners hold channel keys. + No comment provided by engineer. + Select Selecteer @@ -7208,6 +7750,10 @@ chat item action Send request without message No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. Stuur ze vanuit de galerij of aangepaste toetsenborden. @@ -7218,6 +7764,10 @@ chat item action Stuur tot 100 laatste berichten naar nieuwe leden. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + No comment provided by engineer. + Send your private feedback to groups. No comment provided by engineer. @@ -7232,6 +7782,10 @@ chat item action De afzender heeft mogelijk het verbindingsverzoek verwijderd. No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + alert message + 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. @@ -7357,6 +7911,10 @@ chat item action Serverprotocol gewijzigd. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord @@ -7486,6 +8044,14 @@ chat item action Instellingen zijn gewijzigd. alert message + + Setup notifications + No comment provided by engineer. + + + Setup routers + No comment provided by engineer. + Shape profile images Vorm profiel afbeeldingen @@ -7522,11 +8088,14 @@ chat item action Adres openbaar delen No comment provided by engineer. - - Share address with contacts? - Adres delen met contacten? + + Share address with SimpleX contacts? alert title + + Share channel + No comment provided by engineer. + Share from other apps. Delen vanuit andere apps. @@ -7550,6 +8119,10 @@ chat item action Profiel delen No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Deel deze eenmalige uitnodigingslink @@ -7560,9 +8133,12 @@ chat item action Delen op SimpleX No comment provided by engineer. - - Share with contacts - Delen met contacten + + Share via chat + No comment provided by engineer. + + + Share with SimpleX contacts No comment provided by engineer. @@ -7732,8 +8308,8 @@ chat item action SimpleX-protocollen beoordeeld door Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7809,6 +8385,11 @@ report reason Vierkant, cirkel of iets daartussenin. No comment provided by engineer. + + Star on GitHub + Star on GitHub + No comment provided by engineer. + Start chat Begin gesprek @@ -7909,6 +8490,63 @@ report reason Subscribed No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber reports + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers can add message reactions. + No comment provided by engineer. + + + Subscribers can chat with admins. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + No comment provided by engineer. + + + Subscribers can send direct messages. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + No comment provided by engineer. + + + Subscribers can send files and media. + No comment provided by engineer. + + + Subscribers can send voice messages. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Subscription fouten @@ -7988,6 +8626,10 @@ report reason Foto nemen No comment provided by engineer. + + Talk to someone + No comment provided by engineer. + Tap Connect to chat No comment provided by engineer. @@ -8000,9 +8642,8 @@ report reason Tap Connect to use bot No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. - Tik op SimpleX-adres maken in het menu om het later te maken. + + Tap Join channel No comment provided by engineer. @@ -8034,6 +8675,10 @@ report reason Tik hier om incognito lid te worden No comment provided by engineer. + + Tap to open + No comment provided by engineer. + Tap to paste link Tik hier om de link te plakken @@ -8052,13 +8697,18 @@ report reason Test failed at step %@. Test mislukt bij stap %@. - server test failure + relay test failure +server test failure Test notifications Testmeldingen No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Server test @@ -8110,6 +8760,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De app beschermt uw privacy door in elk gesprek andere operatoren te gebruiken. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). De app vraagt om downloads van onbekende bestandsservers (behalve .onion) te bevestigen. @@ -8125,6 +8779,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De code die u heeft gescand is geen SimpleX link QR-code. No comment provided by engineer. + + The connection reached the limit of undelivered messages + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. De verbinding heeft de limiet van niet-afgeleverde berichten bereikt. Uw contactpersoon is mogelijk offline. @@ -8150,9 +8808,9 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De versleuteling werkt en de nieuwe versleutelingsovereenkomst is niet vereist. Dit kan leiden tot verbindingsfouten! No comment provided by engineer. - - The future of messaging - De volgende generatie privéberichten + + The first network where you own +your contacts and groups. No comment provided by engineer. @@ -8189,6 +8847,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De oude database is niet verwijderd tijdens de migratie, deze kan worden verwijderd. No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + No comment provided by engineer. + The same conditions will apply to operator **%@**. Dezelfde voorwaarden gelden voor operator **%@**. @@ -8234,6 +8896,14 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Thema's No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + No comment provided by engineer. + These conditions will also apply for: **%@**. Deze voorwaarden zijn ook van toepassing op: **%@**. @@ -8299,6 +8969,14 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Deze groep bestaat niet meer. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Voor deze link is een nieuwere app-versie vereist. Werk de app bij of vraag je contactpersoon om een compatibele link te sturen. @@ -8347,6 +9025,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Om ongewenste berichten te verbergen. No comment provided by engineer. + + To make SimpleX Network last. + No comment provided by engineer. + To make a new connection Om een nieuwe verbinding te maken @@ -8432,11 +9114,6 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc 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 chat list: - Chatlijst wisselen: - No comment provided by engineer. - Toggle incognito when connecting. Schakel incognito in tijdens het verbinden. @@ -8452,6 +9129,10 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc De transparantie van de werkbalk No comment provided by engineer. + + Top bar + No comment provided by engineer. + Total Totaal @@ -8516,6 +9197,10 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Lid deblokkeren? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Niet afgeleverde berichten @@ -8616,13 +9301,17 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Unsupported connection link Niet-ondersteunde verbindingslink - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. Er worden maximaal 100 laatste berichten naar nieuwe leden verzonden. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + No comment provided by engineer. + Update Update @@ -8742,11 +9431,6 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik TCP-poort 443 alleen voor vooraf ingestelde servers. No comment provided by engineer. - - Use chat - Gebruik chat - No comment provided by engineer. - Use current profile Gebruik het huidige profiel @@ -8762,6 +9446,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik voor berichten No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Gebruik voor nieuwe verbindingen @@ -8801,6 +9489,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik privéroutering met onbekende servers. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Gebruik server @@ -8821,6 +9513,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik de app met één hand. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + No comment provided by engineer. + Use web port Gebruik een webpoort @@ -8841,6 +9537,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik SimpleX Chat servers. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Code verifiëren met desktop @@ -8960,6 +9660,18 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Spraakbericht… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + + + Waiting for channel owner to add relays. + No comment provided by engineer. + Waiting for desktop... Wachten op desktop... @@ -9000,6 +9712,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Waarschuwing: u kunt sommige gegevens verliezen! No comment provided by engineer. + + We made connecting simpler for new users. + No comment provided by engineer. + WebRTC ICE servers WebRTC ICE servers @@ -9049,6 +9765,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen. No comment provided by engineer. + + Why SimpleX is built. + No comment provided by engineer. + WiFi Wifi @@ -9259,6 +9979,10 @@ Deelnameverzoek herhalen? U kunt een voorbeeld van een melding op het vergrendeld scherm instellen via instellingen. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + 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. U kunt een link of een QR-code delen. Iedereen kan lid worden van de groep. U verliest geen leden van de groep als u deze later verwijdert. @@ -9304,16 +10028,21 @@ Deelnameverzoek herhalen? Je kunt geen berichten versturen! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. U kon niet worden geverifieerd; probeer het opnieuw. No comment provided by engineer. - - You decide who can connect. - Jij bepaalt wie er verbinding mag maken. - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -9381,6 +10110,10 @@ Verbindingsverzoek herhalen? U zou meldingen moeten ontvangen. token info + + You were born without an account + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. No comment provided by engineer. @@ -9415,6 +10148,10 @@ Verbindingsverzoek herhalen? U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. U ontvangt geen berichten meer van deze chat. De chatgeschiedenis blijft bewaard. @@ -9459,6 +10196,10 @@ Verbindingsverzoek herhalen? Uw oproepen No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Uw chat database @@ -9507,6 +10248,10 @@ Verbindingsverzoek herhalen? Uw contacten blijven verbonden. No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + No comment provided by engineer. + Your credentials may be sent unencrypted. Uw inloggegevens worden mogelijk niet-versleuteld verzonden. @@ -9526,6 +10271,10 @@ Verbindingsverzoek herhalen? Your group No comment provided by engineer. + + Your network + No comment provided by engineer. + Your preferences Jouw voorkeuren @@ -9541,6 +10290,11 @@ Verbindingsverzoek herhalen? Jouw profiel No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Uw profiel **%@** wordt gedeeld. @@ -9561,11 +10315,23 @@ Verbindingsverzoek herhalen? Je profiel is gewijzigd. Als je het opslaat, wordt het bijgewerkte profiel naar al je contacten verzonden. alert message + + Your public address + No comment provided by engineer. + Your random profile Je willekeurige profiel No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Uw server adres @@ -9581,21 +10347,11 @@ Verbindingsverzoek herhalen? Uw instellingen No comment provided by engineer. - - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - [Bijdragen](https://github.com/simplex-chat/simplex-chat#contribute) - No comment provided by engineer. - [Send us email](mailto:chat@simplex.chat) [Stuur ons een e-mail](mailto:chat@simplex.chat) No comment provided by engineer. - - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - \_italic_ \_cursief_ @@ -9611,6 +10367,10 @@ Verbindingsverzoek herhalen? hier boven, kies dan: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ geaccepteerd %@ @@ -9631,6 +10391,10 @@ Verbindingsverzoek herhalen? heb je geaccepteerd rcv group event chat item + + active + No comment provided by engineer. + admin Beheerder @@ -9742,6 +10506,10 @@ marked deleted chat item preview text bellen… call status + + can't broadcast + No comment provided by engineer. + can't send messages kan geen berichten versturen @@ -9777,6 +10545,14 @@ marked deleted chat item preview text adres wijzigen… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored gekleurd @@ -9922,6 +10698,10 @@ pref value verwijderd deleted chat item + + deleted channel + rcv group event chat item + deleted contact verwijderd contact @@ -10032,6 +10812,10 @@ pref value fout No comment provided by engineer. + + error: %@ + receive error chat item + expired verlopen @@ -10160,6 +10944,10 @@ pref value is vertrokken rcv group event chat item + + link + No comment provided by engineer. + marked deleted gemarkeerd als verwijderd @@ -10230,6 +11018,10 @@ pref value nooit delete after time + + new + No comment provided by engineer. + new message nieuw bericht @@ -10352,6 +11144,10 @@ time to disappear geweigerde oproep call status + + relay + member role + removed verwijderd @@ -10362,6 +11158,14 @@ time to disappear verwijderd %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + + + removed by operator + No comment provided by engineer. + removed contact address contactadres verwijderd @@ -10513,6 +11317,10 @@ laatst ontvangen bericht: %2$@ onbeschermd No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile bijgewerkt groep profiel @@ -10533,6 +11341,10 @@ laatst ontvangen bericht: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link via contact adres link @@ -10608,6 +11420,10 @@ laatst ontvangen bericht: %2$@ je bent waarnemer No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ je hebt %@ geblokkeerd @@ -10668,6 +11484,10 @@ laatst ontvangen bericht: %2$@ \~staking~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + owner verification + 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 ee17f807ba..b232aa84af 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -185,6 +185,21 @@ %d miesięcy time interval + + %d relays failed + channel relay bar +channel subscriber relay bar + + + %d relays not active + channel relay bar +channel subscriber relay bar + + + %d relays removed + channel relay bar +channel subscriber relay bar + %d sec %d sek @@ -200,11 +215,53 @@ %d pominięte wiadomość(i) integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d tygodni time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + channel relay bar + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + channel subscriber relay bar + %lld %lld @@ -215,6 +272,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld wybrany(e) kontakt(y) @@ -315,11 +376,19 @@ %u pominiętych wiadomości. No comment provided by engineer. + + (from owner) + chat link info line + (new) (nowy) No comment provided by engineer. + + (signed) + chat link info line + (this device v%@) (to urządzenie v%@) @@ -365,6 +434,10 @@ **Zeskanuj / Wklej link**: aby połączyć się za pomocą otrzymanego linku. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Uwaga**: Natychmiastowe powiadomienia push wymagają zapisania kodu dostępu w Keychain. @@ -408,6 +481,12 @@ - i więcej! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -506,6 +585,10 @@ time interval Jeszcze kilka rzeczy No comment provided by engineer. + + A link for one person to connect + No comment provided by engineer. + A new contact Nowy kontakt @@ -632,9 +715,8 @@ swipe action Aktywne połączenia 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. - Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów. + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. @@ -702,6 +784,10 @@ swipe action Dodano serwery wiadomości No comment provided by engineer. + + Adding relays will be supported later. + No comment provided by engineer. + Additional accent Dodatkowy akcent @@ -822,6 +908,14 @@ swipe action Wszystkie profile profile dropdown + + All relays failed + No comment provided by engineer. + + + All relays removed + No comment provided by engineer. + All reports will be archived for you. Wszystkie raporty zostaną dla Ciebie zarchiwizowane. @@ -882,6 +976,10 @@ swipe action Zezwalaj na nieodwracalne usuwanie wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli. (24 godziny) No comment provided by engineer. + + Allow members to chat with admins. + No comment provided by engineer. + Allow message reactions only if your contact allows them. Zezwalaj na reakcje wiadomości tylko wtedy, gdy zezwala na to Twój kontakt. @@ -897,6 +995,10 @@ swipe action Zezwalaj na wysyłanie bezpośrednich wiadomości do członków. No comment provided by engineer. + + Allow sending direct messages to subscribers. + No comment provided by engineer. + Allow sending disappearing messages. Zezwól na wysyłanie znikających wiadomości. @@ -907,6 +1009,10 @@ swipe action Zezwól na udostępnianie No comment provided by engineer. + + Allow subscribers to chat with admins. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Zezwól na nieodwracalne usunięcie wysłanych wiadomości. (24 godziny) @@ -1012,11 +1118,6 @@ swipe action Odbierz połączenie No comment provided by engineer. - - Anybody can host servers. - Każdy może hostować serwery. - No comment provided by engineer. - App build: %@ Kompilacja aplikacji: %@ @@ -1222,6 +1323,21 @@ swipe action Zły hash wiadomości No comment provided by engineer. + + Be free +in your network + No comment provided by engineer. + + + Be free in your network. + Ciesz się swobodą w swojej sieci. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + Ponieważ zniszczyliśmy moc pozwalającą poznać, kim jesteś. Więc twoja moc nigdy nie będzie Ci odebrana. + No comment provided by engineer. + Better calls Lepsze połączenia @@ -1317,6 +1433,10 @@ swipe action Zablokować członka? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Zablokowany przez admina @@ -1367,6 +1487,14 @@ swipe action Zarówno Ty, jak i Twój kontakt możecie wysyłać wiadomości głosowe. No comment provided by engineer. + + Bottom bar + No comment provided by engineer. + + + Broadcast + compose placeholder for channel owner + 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)! @@ -1375,7 +1503,7 @@ swipe action Business address Adres firmowy - No comment provided by engineer. + chat link info line Business chats @@ -1397,15 +1525,6 @@ swipe action 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). No comment provided by engineer. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - Korzystając z SimpleX Chat, zgadzasz się: -- wysyłać tylko legalne treści w grupach publicznych. -- szanować innych użytkowników – nie spamować. - No comment provided by engineer. - Call already ended! Połączenie już zakończone! @@ -1554,6 +1673,67 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + alert message +alert subtitle + + + Channel image + No comment provided by engineer. + + + Channel link + chat link info line + + + Channel preferences + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel temporarily unavailable + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + + + Channels + No comment provided by engineer. + Chat Czat @@ -1639,6 +1819,22 @@ set passcode view Profil użytkownika No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Motyw czatu @@ -1657,7 +1853,8 @@ set passcode view Chat with admins Czatuj z administratorami - chat toolbar + chat feature +chat toolbar Chat with member @@ -1674,11 +1871,23 @@ set passcode view Czaty No comment provided by engineer. + + Chats with admins are prohibited. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + alert message + Chats with members Czaty z członkami No comment provided by engineer. + + Chats with members are disabled + No comment provided by engineer. + Check messages every 20 min. Sprawdzaj wiadomości co 20 min. @@ -1689,6 +1898,14 @@ set passcode view Sprawdź wiadomości, gdy będzie to dopuszczone. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Sprawdź adres serwera i spróbuj ponownie. @@ -1834,9 +2051,8 @@ set passcode view Skonfiguruj serwery ICE No comment provided by engineer. - - Configure server operators - Skonfiguruj operatorów serwerów + + Configure relays No comment provided by engineer. @@ -1897,7 +2113,8 @@ set passcode view Connect Połącz - server test step + relay test step +server test step Connect automatically @@ -1943,6 +2160,10 @@ To jest twój jednorazowy link! Połącz się przez link new chat sheet title + + Connect via link or QR code + No comment provided by engineer. + Connect via one-time link Połącz przez jednorazowy link @@ -2021,10 +2242,11 @@ To jest twój jednorazowy link! Connection error (AUTH) Błąd połączenia (UWIERZYTELNIANIE) - No comment provided by engineer. + conn error description Connection failed + Połączenie nie powiodło się No comment provided by engineer. @@ -2079,6 +2301,10 @@ To jest twój jednorazowy link! Połączenia No comment provided by engineer. + + Contact address + chat link info line + Contact allows Kontakt pozwala @@ -2149,6 +2375,11 @@ To jest twój jednorazowy link! Kontynuuj No comment provided by engineer. + + Contribute + Przyczyń się + No comment provided by engineer. + Conversation deleted! Rozmowa usunięta! @@ -2177,12 +2408,7 @@ To jest twój jednorazowy link! Correct name to %@? Poprawić imię na %@? - No comment provided by engineer. - - - Create - Utwórz - No comment provided by engineer. + alert message Create 1-time link @@ -2234,6 +2460,14 @@ To jest twój jednorazowy link! Utwórz profil No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Utwórz kolejkę @@ -2244,11 +2478,19 @@ To jest twój jednorazowy link! Utwórz swój adres No comment provided by engineer. + + Create your link + No comment provided by engineer. + Create your profile Utwórz swój profil No comment provided by engineer. + + Create your public address + No comment provided by engineer. + Created Utworzono @@ -2269,6 +2511,10 @@ To jest twój jednorazowy link! Tworzenie linku archiwum No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Tworzenie linku… @@ -2427,10 +2673,9 @@ To jest twój jednorazowy link! Dostarczenie debugowania No comment provided by engineer. - - Decentralized - Zdecentralizowane - No comment provided by engineer. + + Decode link + relay test step Decryption error @@ -2478,6 +2723,14 @@ swipe action Usuń i powiadom kontakt No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Usuń czat @@ -2649,6 +2902,10 @@ alert button Usuń kolejkę server test step + + Delete relay + No comment provided by engineer. + Delete report Usuń raport @@ -2814,6 +3071,14 @@ alert button Bezpośrednie wiadomości między członkami są zabronione w tej grupie. No comment provided by engineer. + + Direct messages between subscribers are prohibited. + No comment provided by engineer. + + + Disable + alert button + Disable (keep overrides) Wyłącz (zachowaj nadpisania) @@ -2919,6 +3184,10 @@ alert button Nie wysyłaj historii do nowych członków. No comment provided by engineer. + + Do not send history to new subscribers. + No comment provided by engineer. + Do not use credentials with proxy. Nie używaj danych logowania do proxy. @@ -3020,11 +3289,19 @@ chat item action Powiadomienia szyfrowane E2E. No comment provided by engineer. + + Easier to invite your friends 👋 + No comment provided by engineer. + Edit Edytuj chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Edytuj profil grupy @@ -3038,7 +3315,7 @@ chat item action Enable Włącz - No comment provided by engineer. + alert button Enable (keep overrides) @@ -3060,6 +3337,10 @@ chat item action Włącz utrzymywanie aktywności TCP No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Czy włączyć automatyczne usuwanie wiadomości? @@ -3070,6 +3351,10 @@ chat item action Włącz dostęp do kamery No comment provided by engineer. + + Enable chats with admins? + alert title + Enable disappearing messages by default. Włącz domyślnie znikające wiadomości. @@ -3090,16 +3375,15 @@ chat item action Włączyć natychmiastowe powiadomienia? No comment provided by engineer. + + Enable link previews? + alert title + Enable lock Włącz blokadę No comment provided by engineer. - - Enable notifications - Włącz powiadomienia - No comment provided by engineer. - Enable periodic notifications? Włączyć okresowe powiadomienia? @@ -3205,6 +3489,10 @@ chat item action Wprowadź Pin No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Wprowadź poprawne hasło. @@ -3230,6 +3518,14 @@ chat item action Wprowadź hasło powyżej, aby pokazać! No comment provided by engineer. + + Enter profile name... + No comment provided by engineer. + + + Enter relay name… + No comment provided by engineer. + Enter server manually Wprowadź serwer ręcznie @@ -3258,7 +3554,7 @@ chat item action Error Błąd - No comment provided by engineer. + conn error description Error aborting address change @@ -3285,6 +3581,10 @@ chat item action Błąd dodawania członka(ów) No comment provided by engineer. + + Error adding relay + alert title + Error adding server Błąd podczas dodawania serwera @@ -3345,6 +3645,10 @@ chat item action Błąd tworzenia adresu No comment provided by engineer. + + Error creating channel + alert title + Error creating group Błąd tworzenia grupy @@ -3480,11 +3784,6 @@ chat item action Błąd otwierania czatu No comment provided by engineer. - - Error opening group - Błąd otwierania grupy - No comment provided by engineer. - Error receiving file Błąd odbioru pliku @@ -3530,6 +3829,10 @@ chat item action Błąd zapisu serwerów ICE No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Błąd zapisywania listy czatów @@ -3595,6 +3898,10 @@ chat item action Błąd ustawiania potwierdzeń dostawy! No comment provided by engineer. + + Error sharing channel + alert title + Error starting chat Błąd uruchamiania czatu @@ -3675,7 +3982,8 @@ snd error text Error: %@. Błąd: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3919,7 +4227,8 @@ snd error text Fingerprint in server address does not match certificate. Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3961,10 +4270,15 @@ snd error text Dla wszystkich moderatorów No comment provided by engineer. + + For anyone to reach you + No comment provided by engineer. + For chat profile %@: Dla profilu czatu %@: - servers error + servers error +servers warning For console @@ -4105,11 +4419,19 @@ Błąd: %2$@ GIF-y i naklejki No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. Otrzymuj powiadomienia, gdy ktoś wspomni o Tobie. No comment provided by engineer. + + Get started + No comment provided by engineer. + Good afternoon! Dzień dobry! @@ -4168,7 +4490,7 @@ Błąd: %2$@ Group link Link do grupy - No comment provided by engineer. + chat link info line Group links @@ -4280,6 +4602,10 @@ Błąd: %2$@ Historia nie jest wysyłana do nowych członków. No comment provided by engineer. + + History is not sent to new subscribers. + No comment provided by engineer. + How SimpleX works Jak działa SimpleX @@ -4347,6 +4673,7 @@ Błąd: %2$@ If you joined or created channels, they will stop working permanently. + Jeśli dołączyłeś do kanałów lub je utworzyłeś, przestaną one działać na stałe. down migration warning @@ -4379,11 +4706,6 @@ Błąd: %2$@ Natychmiast No comment provided by engineer. - - Immune to spam - Odporność na spam i nadużycia - No comment provided by engineer. - Import Importuj @@ -4526,9 +4848,9 @@ Wkrótce pojawią się kolejne ulepszenia! Rola początkowa No comment provided by engineer. - - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - Zainstaluj [SimpleX Chat na terminal](https://github.com/simplex-chat/simplex-chat) + + Install SimpleX Chat for terminal + Zainstaluj SimpleX Chat na terminal No comment provided by engineer. @@ -4586,7 +4908,7 @@ Wkrótce pojawią się kolejne ulepszenia! Invalid connection link Nieprawidłowy link połączenia - No comment provided by engineer. + conn error description Invalid display name! @@ -4606,7 +4928,15 @@ Wkrótce pojawią się kolejne ulepszenia! Invalid name! Nieprawidłowa nazwa! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4643,6 +4973,10 @@ Wkrótce pojawią się kolejne ulepszenia! Zaproś członków No comment provided by engineer. + + Invite someone privately + No comment provided by engineer. + Invite to chat Zaproś do czatu @@ -4719,6 +5053,10 @@ Wkrótce pojawią się kolejne ulepszenia! dołącz jako %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Dołącz do grupy @@ -4806,6 +5144,14 @@ To jest twój link do grupy %@! Opuść swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Opuść czat @@ -4831,6 +5177,10 @@ To jest twój link do grupy %@! Mniejszy ruch w sieciach komórkowych. No comment provided by engineer. + + Let someone connect to you + No comment provided by engineer. + Let's talk in SimpleX Chat Porozmawiajmy w SimpleX Chat @@ -4851,6 +5201,10 @@ To jest twój link do grupy %@! Połącz mobile i komputerowe aplikacje! 🔗 No comment provided by engineer. + + Link signature verified. + owner verification + Linked desktop options Połączone opcje komputera @@ -5036,6 +5390,10 @@ To jest twój link do grupy %@! Członkowie grupy mogą dodawać reakcje wiadomości. No comment provided by engineer. + + Members can chat with admins. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny) @@ -5101,6 +5459,10 @@ To jest twój link do grupy %@! Wersja robocza wiadomości No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Wiadomość przekazana @@ -5196,6 +5558,14 @@ To jest twój link do grupy %@! Wiadomości od %@ zostaną pokazane! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + E2EE info chat item + Messages in this chat will never be deleted. Wiadomości na tym czacie nigdy nie zostaną usunięte. @@ -5226,16 +5596,15 @@ To jest twój link do grupy %@! Wiadomości, pliki i połączenia są chronione przez **kwantowo odporne szyfrowanie end-to-end** z doskonałym utajnianiem z wyprzedzeniem i odzyskiem po złamaniu. No comment provided by engineer. + + Migrate + No comment provided by engineer. + Migrate device Zmigruj urządzenie No comment provided by engineer. - - Migrate from another device - Zmigruj z innego urządzenia - No comment provided by engineer. - Migrate here Zmigruj tutaj @@ -5356,6 +5725,10 @@ To jest twój link do grupy %@! Sieć i serwery No comment provided by engineer. + + Network commitments + No comment provided by engineer. + Network connection Połączenie z siecią @@ -5366,6 +5739,10 @@ To jest twój link do grupy %@! Decentralizacja sieci No comment provided by engineer. + + Network error + conn error description + Network issues - message expired after many attempts to send it. Błąd sieciowy - wiadomość wygasła po wielu próbach wysłania jej. @@ -5381,6 +5758,11 @@ To jest twój link do grupy %@! Operator sieci No comment provided by engineer. + + Network routers cannot know +who talks to whom + No comment provided by engineer. + Network settings Ustawienia sieci @@ -5396,6 +5778,10 @@ To jest twój link do grupy %@! Nowy token status text + + New 1-time link + No comment provided by engineer. + New Passcode Nowy Pin @@ -5421,6 +5807,10 @@ To jest twój link do grupy %@! Nowe możliwości czatu 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Nowa prośba o kontakt @@ -5491,11 +5881,28 @@ To jest twój link do grupy %@! Nie No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + No comment provided by engineer. + + + No active relays + No comment provided by engineer. + No app password Brak hasła aplikacji Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats Żadnych czatów @@ -5641,11 +6048,24 @@ To jest twój link do grupy %@! Brak nieprzeczytanych czatów No comment provided by engineer. - - No user identifiers. - Brak identyfikatorów użytkownika. + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. + Nikt nie śledził twoich rozmów. Nikt nie rysował mapy miejsc, w których byłeś. Prywatność nigdy nie była funkcją - była sposobem na życie. No comment provided by engineer. + + Non-profit governance + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + Nie chodzi o lepszy zamek w drzwiach kogoś innego. Nie chodzi o milszego właściciela, który szanuje twoją prywatność, ale nadal prowadzi rejestr wszystkich odwiedzających. Nie jesteś gościem. Jesteś w domu. Żaden król nie może do niego wejść - jesteś suwerenem. + No comment provided by engineer. + + + Not all relays connected + alert title + Not compatible! Nie kompatybilny! @@ -5703,7 +6123,7 @@ To jest twój link do grupy %@! OK OK - No comment provided by engineer. + alert button Off @@ -5722,11 +6142,19 @@ new chat action Stara baza danych No comment provided by engineer. + + On your phone, not on servers. + No comment provided by engineer. + One-time invitation link Jednorazowy link zaproszenia No comment provided by engineer. + + One-time link + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5746,6 +6174,10 @@ Wymaga włączenia VPN. Hosty onion nie będą używane. No comment provided by engineer. + + Only channel owners can change channel preferences. + No comment provided by engineer. + Only chat owners can change preferences. Tylko właściciele czatu mogą zmieniać preferencje. @@ -5849,7 +6281,8 @@ Wymaga włączenia VPN. Open Otwórz - alert action + alert action +alert button Open Settings @@ -5861,6 +6294,10 @@ Wymaga włączenia VPN. Otwórz zmiany No comment provided by engineer. + + Open channel + new chat action + Open chat Otwórz czat @@ -5881,6 +6318,10 @@ Wymaga włączenia VPN. Otwórz warunki No comment provided by engineer. + + Open external link? + alert title + Open full link Otwórz pełny link @@ -5901,6 +6342,10 @@ Wymaga włączenia VPN. Otwórz migrację na innym urządzeniu authentication reason + + Open new channel + new chat action + Open new chat Otwórz nowy czat @@ -5946,6 +6391,13 @@ Wymaga włączenia VPN. Serwer Operatora alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + No comment provided by engineer. + Or import archive file Lub zaimportuj plik archiwalny @@ -5966,6 +6418,10 @@ Wymaga włączenia VPN. Lub bezpiecznie udostępnij ten link pliku No comment provided by engineer. + + Or show QR in person or via video call. + No comment provided by engineer. + Or show this code Lub pokaż ten kod @@ -5976,6 +6432,10 @@ Wymaga włączenia VPN. Lub udostępnij prywatnie No comment provided by engineer. + + Or use this QR - print or show online. + No comment provided by engineer. + Organize chats into lists Organizuj czaty jako listy @@ -5993,6 +6453,18 @@ Wymaga włączenia VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + + + Ownership: you can run your own relays. + No comment provided by engineer. + PING count Liczba PINGÓW @@ -6048,6 +6520,10 @@ Wymaga włączenia VPN. Wklej obraz No comment provided by engineer. + + Paste link / Scan + No comment provided by engineer. + Paste link to connect! Wklej link, aby połączyć! @@ -6202,6 +6678,14 @@ Błąd: %@ Zachowaj ostatnią wersję roboczą wiadomości wraz z załącznikami. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Wstępnie ustawiony adres serwera @@ -6237,14 +6721,12 @@ Błąd: %@ Polityka prywatności i warunki korzystania. No comment provided by engineer. - - Privacy redefined - Redefinicja prywatności + + Privacy: for owners and subscribers. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. - Prywatne czaty, grupy i Twoje kontakty nie są dostępne dla operatorów serwerów. + + Private and secure messaging. No comment provided by engineer. @@ -6287,6 +6769,10 @@ Błąd: %@ Limit czasu routingu prywatnego alert title + + Proceed + alert action + Profile and server connections Profil i połączenia z serwerem @@ -6312,9 +6798,8 @@ Błąd: %@ Motyw profilu No comment provided by engineer. - - Profile update will be sent to your contacts. - Aktualizacja profilu zostanie wysłana do Twoich kontaktów. + + Profile update will be sent to your SimpleX contacts. alert message @@ -6322,6 +6807,10 @@ Błąd: %@ Zabroń połączeń audio/wideo. No comment provided by engineer. + + Prohibit chats with admins. + No comment provided by engineer. + Prohibit irreversible message deletion. Zabroń nieodwracalnego usuwania wiadomości. @@ -6352,6 +6841,10 @@ Błąd: %@ Zabroń wysyłania bezpośrednich wiadomości do członków. No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + No comment provided by engineer. + Prohibit sending disappearing messages. Zabroń wysyłania znikających wiadomości. @@ -6419,6 +6912,10 @@ Włącz w ustawianiach *Sieć i serwery* . Proxy wymaga hasła No comment provided by engineer. + + Public channels - speak freely 🚀 + No comment provided by engineer. + Push notifications Powiadomienia push @@ -6459,24 +6956,14 @@ Włącz w ustawianiach *Sieć i serwery* . Przeczytaj więcej No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - Przeczytaj więcej w [Poradniku Użytkownika](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + Przeczytaj więcej w Poradniku Użytkownika. No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - No comment provided by engineer. - - - Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - Przeczytaj więcej w [Podręczniku Użytkownika](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). - Przeczytaj więcej na naszym [repozytorium GitHub](https://github.com/simplex-chat/simplex-chat#readme). + + Read more in our GitHub repository. + Przeczytaj więcej na naszym repozytorium GitHub. No comment provided by engineer. @@ -6636,6 +7123,26 @@ swipe action Odrzucić członka? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + + + Relay results: + alert message + Relay server is only used if necessary. Another party can observe your IP address. Serwer przekaźnikowy jest używany tylko w razie potrzeby. Inna strona może obserwować Twój adres IP. @@ -6646,6 +7153,14 @@ swipe action Serwer przekaźnikowy chroni Twój adres IP, ale może obserwować czas trwania połączenia. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + + + Reliability: many relays per channel. + No comment provided by engineer. + Remove Usuń @@ -6686,6 +7201,14 @@ swipe action Usunąć hasło z pęku kluczy? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. Usuwa wiadomości i blokuje członków. @@ -6921,6 +7444,10 @@ swipe action Proxy SOCKS No comment provided by engineer. + + Safe web links + No comment provided by engineer. + Safely receive files Bezpiecznie otrzymuj pliki @@ -6947,6 +7474,10 @@ chat item action Zapisz (i powiadom członków) alert button + + Save (and notify subscribers) + alert button + Save admission settings? Zapisać ustawienia wstępu? @@ -6962,6 +7493,10 @@ chat item action Zapisz i powiadom członków grupy No comment provided by engineer. + + Save and notify subscribers + No comment provided by engineer. + Save and reconnect Zapisz i połącz ponownie @@ -6972,6 +7507,14 @@ chat item action Zapisz i zaktualizuj profil grupowy No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Zapisz profil grupy @@ -7152,6 +7695,10 @@ chat item action Kod bezpieczeństwa No comment provided by engineer. + + Security: owners hold channel keys. + No comment provided by engineer. + Select Wybierz @@ -7282,6 +7829,10 @@ chat item action Wyślij prośbę bez wiadomości No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. Wyślij je z galerii lub niestandardowych klawiatur. @@ -7292,6 +7843,10 @@ chat item action Wysyłaj do 100 ostatnich wiadomości do nowych członków. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + No comment provided by engineer. + Send your private feedback to groups. Wyślij swoją prywatną opinię do grup. @@ -7307,6 +7862,10 @@ chat item action Nadawca mógł usunąć prośbę o połączenie. No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. Wysyłanie potwierdzeń dostawy zostanie włączone dla wszystkich kontaktów we wszystkich widocznych profilach czatu. @@ -7432,6 +7991,10 @@ chat item action Protokół serwera zmieniony. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło. @@ -7562,6 +8125,14 @@ chat item action Ustawienia zostały zmienione. alert message + + Setup notifications + No comment provided by engineer. + + + Setup routers + No comment provided by engineer. + Shape profile images Kształtuj obrazy profilowe @@ -7598,11 +8169,14 @@ chat item action Udostępnij adres publicznie No comment provided by engineer. - - Share address with contacts? - Udostępnić adres kontaktom? + + Share address with SimpleX contacts? alert title + + Share channel + No comment provided by engineer. + Share from other apps. Udostępnij z innych aplikacji. @@ -7628,6 +8202,10 @@ chat item action Udostępnij profil No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Udostępnij ten jednorazowy link @@ -7638,9 +8216,12 @@ chat item action Udostępnij do SimpleX No comment provided by engineer. - - Share with contacts - Udostępnij kontaktom + + Share via chat + No comment provided by engineer. + + + Share with SimpleX contacts No comment provided by engineer. @@ -7813,9 +8394,8 @@ chat item action Protokoły SimpleX sprawdzone przez Trail of Bits. No comment provided by engineer. - - SimpleX relay link - łącze przekaźnikowe SimpleX + + SimpleX relay address simplex link type @@ -7891,6 +8471,11 @@ report reason Kwadrat, okrąg lub cokolwiek pomiędzy. No comment provided by engineer. + + Star on GitHub + Daj gwiazdkę na GitHub + No comment provided by engineer. + Start chat Rozpocznij czat @@ -7991,6 +8576,63 @@ report reason Zasubskrybowano No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber reports + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers can add message reactions. + No comment provided by engineer. + + + Subscribers can chat with admins. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + No comment provided by engineer. + + + Subscribers can send direct messages. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + No comment provided by engineer. + + + Subscribers can send files and media. + No comment provided by engineer. + + + Subscribers can send voice messages. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Błędy subskrypcji @@ -8071,6 +8713,10 @@ report reason Zrób zdjęcie No comment provided by engineer. + + Talk to someone + No comment provided by engineer. + Tap Connect to chat Dotknij Połącz aby rozpocząć czat @@ -8086,9 +8732,8 @@ report reason Dotknij Połącz aby użyć bota No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. - Dotknij Stwórz adres SimpleX w menu aby utworzyć go później. + + Tap Join channel No comment provided by engineer. @@ -8121,6 +8766,10 @@ report reason Dotnij, aby dołączyć w trybie incognito No comment provided by engineer. + + Tap to open + No comment provided by engineer. + Tap to paste link Dotknij, aby wkleić link @@ -8139,13 +8788,18 @@ report reason Test failed at step %@. Test nie powiódł się na etapie %@. - server test failure + relay test failure +server test failure Test notifications Powiadomienia testowe No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Przetestuj serwer @@ -8198,6 +8852,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Aplikacja chroni Twoją prywatność, korzystając z różnych operatorów w każdej rozmowie. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Aplikacja zapyta o potwierdzenie pobierania od nieznanych serwerów plików (poza .onion). @@ -8213,6 +8871,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Kod, który zeskanowałeś nie jest kodem QR linku SimpleX. No comment provided by engineer. + + The connection reached the limit of undelivered messages + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. Połączenie osiągnęło limit niedostarczonych wiadomości, Twój kontakt może być offline. @@ -8238,9 +8900,9 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Szyfrowanie działa, a nowe uzgodnienie szyfrowania nie jest wymagane. Może to spowodować błędy w połączeniu! No comment provided by engineer. - - The future of messaging - Następna generacja prywatnych wiadomości + + The first network where you own +your contacts and groups. No comment provided by engineer. @@ -8278,6 +8940,11 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Stara baza danych nie została usunięta podczas migracji, można ją usunąć. No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + Najstarsza ludzka wolność - możliwość rozmowy z inną osobą bez bycia obserwowanym - opiera się na infrastrukturze, która nie może jej zdradzić. + No comment provided by engineer. + The same conditions will apply to operator **%@**. Te same warunki będą miały zastosowanie do operatora **%@**. @@ -8323,6 +8990,16 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Motywy No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + Następnie przenieśliśmy się do sieci, a każda platforma prosiła o podanie danych osobowych - imienia i nazwiska, numeru telefonu, znajomych. Zaakceptowaliśmy fakt, że ceną za możliwość komunikowania się z innymi jest ujawnienie komuś, z kim rozmawiamy. Tak było w przypadku każdego pokolenia, ludzi i technologii - telefonu, poczty elektronicznej, komunikatorów, mediów społecznościowych. Wydawało się to jedyną możliwą opcją. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + Jest jeszcze inny sposób. Sieć bez numerów telefonów. Bez nazw użytkowników. Bez kont. Bez jakichkolwiek tożsamości użytkowników. Sieć, która łączy ludzi i przesyła zaszyfrowane wiadomości, nie wiedząc, kto jest podłączony. + No comment provided by engineer. + These conditions will also apply for: **%@**. Warunki te będą miały również zastosowanie w przypadku: **%@**. @@ -8388,6 +9065,14 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Ta grupa już nie istnieje. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Ten link wymaga nowszej wersji aplikacji. Zaktualizuj aplikację lub poproś osobę kontaktową o przesłanie kompatybilnego łącza. @@ -8438,6 +9123,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Aby ukryć niechciane wiadomości. No comment provided by engineer. + + To make SimpleX Network last. + No comment provided by engineer. + To make a new connection Aby nawiązać nowe połączenie @@ -8525,11 +9214,6 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Aby zweryfikować szyfrowanie end-to-end z Twoim kontaktem porównaj (lub zeskanuj) kod na waszych urządzeniach. No comment provided by engineer. - - Toggle chat list: - Przełącz listę czatów: - No comment provided by engineer. - Toggle incognito when connecting. Przełącz incognito przy połączeniu. @@ -8545,6 +9229,10 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Nieprzezroczystość paska narzędzi No comment provided by engineer. + + Top bar + No comment provided by engineer. + Total Łącznie @@ -8610,6 +9298,10 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Odblokować członka? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Niedostarczone wiadomości @@ -8710,13 +9402,17 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Unsupported connection link Nieobsługiwane łącze połączenia - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. Do nowych członków wysyłanych jest do 100 ostatnich wiadomości. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + No comment provided by engineer. + Update Aktualizuj @@ -8842,11 +9538,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Używaj portu TCP 443 tylko dla domyślnych serwerów. No comment provided by engineer. - - Use chat - Użyj czatu - No comment provided by engineer. - Use current profile Użyj obecnego profilu @@ -8862,6 +9553,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Użyj dla wiadomości No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Użyj dla nowych połączeń @@ -8902,6 +9597,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Używaj prywatnego trasowania z nieznanymi serwerami. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Użyj serwera @@ -8922,6 +9621,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Korzystaj z aplikacji jedną ręką. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + No comment provided by engineer. + Use web port Użyj portu internetowego @@ -8942,6 +9645,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Używanie serwerów SimpleX Chat. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Zweryfikuj kod z komputera @@ -9062,6 +9769,18 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Wiadomość głosowa… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + + + Waiting for channel owner to add relays. + No comment provided by engineer. + Waiting for desktop... Oczekiwanie na komputer... @@ -9102,6 +9821,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Uwaga: możesz stracić niektóre dane! No comment provided by engineer. + + We made connecting simpler for new users. + No comment provided by engineer. + WebRTC ICE servers Serwery WebRTC ICE @@ -9152,6 +9875,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Gdy udostępnisz komuś profil incognito, będzie on używany w grupach, do których Cię zaprosi. No comment provided by engineer. + + Why SimpleX is built. + No comment provided by engineer. + WiFi WiFi @@ -9364,6 +10091,10 @@ Powtórzyć prośbę dołączenia? Podgląd powiadomień na ekranie blokady można ustawić w ustawieniach. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + 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. Możesz udostępnić link lub kod QR - każdy będzie mógł dołączyć do grupy. Nie stracisz członków grupy, jeśli później ją usuniesz. @@ -9409,16 +10140,21 @@ Powtórzyć prośbę dołączenia? Nie możesz wysyłać wiadomości! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Nie można zweryfikować użytkownika; proszę spróbować ponownie. No comment provided by engineer. - - You decide who can connect. - Ty decydujesz, kto może się połączyć. - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -9486,6 +10222,11 @@ Powtórzyć prośbę połączenia? Powinieneś otrzymywać powiadomienia. token info + + You were born without an account + Urodziłeś się bez konta. + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. Będziesz mógł wysyłać wiadomości **dopiero po zaakceptowaniu Twojej prośby**. @@ -9521,6 +10262,10 @@ Powtórzyć prośbę połączenia? Nadal będziesz otrzymywać połączenia i powiadomienia z wyciszonych profili, gdy są one aktywne. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Przestaniesz otrzymywać wiadomości z tego czatu. Historia czatu zostanie zachowana. @@ -9566,6 +10311,10 @@ Powtórzyć prośbę połączenia? Twoje połączenia No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Twoja baza danych czatu @@ -9616,6 +10365,11 @@ Powtórzyć prośbę połączenia? Twoje kontakty pozostaną połączone. No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + Twoje rozmowy należą do Ciebie, tak jak zawsze było przed pojawieniem się Internetu. Sieć nie jest miejscem, które odwiedzasz. Jest miejscem, które tworzysz i które należy do Ciebie. Nikt nie może Ci tego odebrać, niezależnie od tego, czy jest to miejsce prywatne, czy publiczne. + No comment provided by engineer. + Your credentials may be sent unencrypted. Twoje poświadczenia mogą zostać wysłane niezaszyfrowane. @@ -9636,6 +10390,10 @@ Powtórzyć prośbę połączenia? Twoja grupa No comment provided by engineer. + + Your network + No comment provided by engineer. + Your preferences Twoje preferencje @@ -9651,6 +10409,11 @@ Powtórzyć prośbę połączenia? Twój profil No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Twój profil **%@** zostanie udostępniony. @@ -9671,11 +10434,23 @@ Powtórzyć prośbę połączenia? Twój profil został zmieniony. Jeśli go zapiszesz, zaktualizowany profil zostanie wysłany do wszystkich kontaktów. alert message + + Your public address + No comment provided by engineer. + Your random profile Twój losowy profil No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Twój adres serwera @@ -9691,21 +10466,11 @@ Powtórzyć prośbę połączenia? Twoje ustawienia No comment provided by engineer. - - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - [Przyczyń się](https://github.com/simplex-chat/simplex-chat#contribute) - No comment provided by engineer. - [Send us email](mailto:chat@simplex.chat) [Wyślij do nas email](mailto:chat@simplex.chat) No comment provided by engineer. - - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - [Daj gwiazdkę na GitHub](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - \_italic_ \_kursywa_ @@ -9721,6 +10486,10 @@ Powtórzyć prośbę połączenia? powyżej, a następnie wybierz: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ zaakceptowano %@ @@ -9741,6 +10510,10 @@ Powtórzyć prośbę połączenia? przyjął cię rcv group event chat item + + active + No comment provided by engineer. + admin administrator @@ -9852,6 +10625,10 @@ marked deleted chat item preview text dzwonie… call status + + can't broadcast + No comment provided by engineer. + can't send messages nie można wysłać wiadomości @@ -9887,6 +10664,14 @@ marked deleted chat item preview text zmiana adresu… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored kolorowy @@ -10033,6 +10818,10 @@ pref value usunięty deleted chat item + + deleted channel + rcv group event chat item + deleted contact usunięto kontakt @@ -10143,6 +10932,10 @@ pref value błąd No comment provided by engineer. + + error: %@ + receive error chat item + expired wygasły @@ -10150,6 +10943,7 @@ pref value failed + nieudane No comment provided by engineer. @@ -10272,6 +11066,10 @@ pref value opuścił rcv group event chat item + + link + No comment provided by engineer. + marked deleted zaznaczona jako usunięta @@ -10342,6 +11140,10 @@ pref value nigdy delete after time + + new + No comment provided by engineer. + new message nowa wiadomość @@ -10465,6 +11267,10 @@ time to disappear odrzucone połączenie call status + + relay + member role + removed usunięty @@ -10475,6 +11281,14 @@ time to disappear usunięto %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + + + removed by operator + No comment provided by engineer. + removed contact address usunięto adres kontaktu @@ -10629,6 +11443,10 @@ ostatnia otrzymana wiadomość: %2$@ niezabezpieczony No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile zaktualizowano profil grupy @@ -10649,6 +11467,10 @@ ostatnia otrzymana wiadomość: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link przez link adresu kontaktu @@ -10724,6 +11546,10 @@ ostatnia otrzymana wiadomość: %2$@ jesteś obserwatorem No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ zablokowałeś %@ @@ -10784,6 +11610,10 @@ ostatnia otrzymana wiadomość: %2$@ \~strajk~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + owner verification + 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 d9a5c48dda..a438327ba1 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -167,7 +167,7 @@ %d hours - %d ч. + %d ч time interval @@ -182,9 +182,27 @@ %d months - %d мес. + %d мес time interval + + %d relays failed + %d релеев с ошибками + channel relay bar +channel subscriber relay bar + + + %d relays not active + %d релеев неактивны + channel relay bar +channel subscriber relay bar + + + %d relays removed + %d релеев удалены + channel relay bar +channel subscriber relay bar + %d sec %d сек @@ -200,11 +218,63 @@ %d пропущенных сообщение(й) integrity error chat item + + %d subscriber + %d подписчик + channel subscriber count + + + %d subscribers + %d подписчиков + channel subscriber count + %d weeks %d недель time interval + + %1$d/%2$d relays active + %1$d/%2$d релеев активны + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + %1$d/%2$d релеев активны, %3$d с ошибками + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + %1$d/%2$d релеев активны, %3$d с ошибками + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + %1$d/%2$d релеев активны, %3$d удалены + channel relay bar + + + %1$d/%2$d relays connected + %1$d/%2$d релеев подключены + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + %1$d/%2$d релеев подключены, %3$d с ошибками + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + %1$d/%2$d релеев подключены, %3$d с ошибками + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + %1$d/%2$d релеев подключены, %3$d удалены + channel subscriber relay bar + %lld %lld @@ -215,6 +285,11 @@ %lld %@ No comment provided by engineer. + + %lld channel events + %lld событий канала + No comment provided by engineer. + %lld contact(s) selected Выбрано контактов: %lld @@ -242,7 +317,7 @@ %lld messages blocked by admin - %lld сообщений заблокировано администратором + %lld сообщений заблокировано админом No comment provided by engineer. @@ -257,7 +332,7 @@ %lld minutes - %lld минуты + %lld минут(ы) No comment provided by engineer. @@ -315,11 +390,21 @@ %u сообщений пропущено. No comment provided by engineer. + + (from owner) + (от владельца) + chat link info line + (new) (новое) No comment provided by engineer. + + (signed) + (с подписью) + chat link info line + (this device v%@) (это устройство v%@) @@ -352,7 +437,7 @@ **Please note**: you will NOT be able to recover or change passphrase if you lose it. - **Внимание**: Вы не сможете восстановить или поменять пароль, если Вы его потеряете. + **Внимание**: Вы не сможете восстановить или поменять пароль, если потеряете его. No comment provided by engineer. @@ -365,14 +450,19 @@ **Сканировать / Вставить ссылку**: чтобы соединиться через полученную ссылку. No comment provided by engineer. + + **Test relay** to retrieve its name. + **Протестируйте релей**, чтобы получить его имя. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. - **Внимание**: для работы мгновенных уведомлений пароль должен быть сохранен в Keychain. + **Внимание**: для работы мгновенных уведомлений пароль должен быть сохранён в Keychain. No comment provided by engineer. **Warning**: the archive will be removed. - **Внимание**: архив будет удален. + **Внимание**: архив будет удалён. No comment provided by engineer. @@ -395,7 +485,7 @@ - delivery receipts (up to 20 members). - 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)! -- отчеты о доставке (до 20 членов). +- отчёты о доставке (до 20 членов). - быстрее и стабильнее. No comment provided by engineer. @@ -408,6 +498,15 @@ - и прочее! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + - включение картинок ссылок. +- защита от фишинга. +- удаление трекинга ссылок. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -503,7 +602,12 @@ time interval A few more things - Еще несколько изменений + Ещё несколько изменений + No comment provided by engineer. + + + A link for one person to connect + Ссылка для одного человека No comment provided by engineer. @@ -524,7 +628,7 @@ time interval 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. - Будет использовано отдельное TCP соединение **для каждого контакта и члена группы**. + Будет использовано отдельное TCP-соединение **для каждого контакта и члена группы**. **Примечание**: Чем больше подключений, тем быстрее разряжается батарея и расходуется трафик, а некоторые соединения могут отваливаться. No comment provided by engineer. @@ -604,7 +708,7 @@ swipe action Accept member - Принять члена + Принять члена группы alert title @@ -632,9 +736,9 @@ swipe action Активные соединения 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. - Добавьте адрес в свой профиль, чтобы Ваши контакты могли поделиться им. Профиль будет отправлен Вашим контактам. + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. + Добавьте адрес в свой профиль, чтобы Ваши SimpleX контакты могли поделиться им. Профиль будет отправлен Вашим SimpleX контактам. No comment provided by engineer. @@ -649,7 +753,7 @@ swipe action Add message - Добавить cообщение + Добавить сообщение placeholder for sending contact request @@ -664,7 +768,7 @@ swipe action Add servers by scanning QR codes. - Добавить серверы через QR код. + Добавить серверы через QR-код. No comment provided by engineer. @@ -702,6 +806,11 @@ swipe action Дополнительные серверы сообщений No comment provided by engineer. + + Adding relays will be supported later. + Добавление релеев будет поддерживаться позже. + No comment provided by engineer. + Additional accent Дополнительный акцент @@ -754,7 +863,7 @@ swipe action Advanced settings - Настройки сети + Дополнительные настройки No comment provided by engineer. @@ -774,7 +883,7 @@ swipe action All chats will be removed from the list %@, and the list deleted. - Все чаты будут удалены из списка %@, и список удален. + Все чаты будут удалены из списка %@, и список удалён. alert message @@ -794,11 +903,12 @@ swipe action All messages + Все сообщения No comment provided by engineer. All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. - Все сообщения и файлы отправляются с **end-to-end шифрованием**, с постквантовой безопасностью в прямых разговорах. + Все сообщения и файлы отправляются с **сквозным шифрованием**, с пост-квантовой безопасностью в прямых разговорах. No comment provided by engineer. @@ -821,6 +931,16 @@ swipe action Все профили profile dropdown + + All relays failed + Все релеи недоступны + No comment provided by engineer. + + + All relays removed + Все релеи удалены + No comment provided by engineer. + All reports will be archived for you. Все сообщения о нарушениях будут заархивированы для вас. @@ -838,12 +958,12 @@ swipe action All your contacts will remain connected. Profile update will be sent to your contacts. - Все Ваши контакты сохранятся. Обновленный профиль будет отправлен Вашим контактам. + Все Ваши контакты сохранятся. Обновлённый профиль будет отправлен Вашим контактам. No comment provided by engineer. All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. - Все ваши контакты, разговоры и файлы будут надежно зашифрованы и загружены на выбранные XFTP серверы. + Все ваши контакты, разговоры и файлы будут надёжно зашифрованы и загружены на выбранные XFTP-серверы. No comment provided by engineer. @@ -881,6 +1001,11 @@ swipe action Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам. (24 часа) No comment provided by engineer. + + Allow members to chat with admins. + Разрешить членам группы общаться с админами. + No comment provided by engineer. + Allow message reactions only if your contact allows them. Разрешить реакции на сообщения, только если ваш контакт разрешает их. @@ -896,6 +1021,11 @@ swipe action Разрешить личные сообщения членам группы. No comment provided by engineer. + + Allow sending direct messages to subscribers. + Разрешить отправку личных сообщений подписчикам. + No comment provided by engineer. + Allow sending disappearing messages. Разрешить посылать исчезающие сообщения. @@ -906,6 +1036,11 @@ swipe action Разрешить поделиться No comment provided by engineer. + + Allow subscribers to chat with admins. + Разрешить подписчикам общаться с админами. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Разрешить необратимо удалять отправленные сообщения. (24 часа) @@ -993,7 +1128,7 @@ swipe action Always use relay - Всегда соединяться через relay + Всегда соединяться через релей No comment provided by engineer. @@ -1011,11 +1146,6 @@ swipe action Принять звонок No comment provided by engineer. - - Anybody can host servers. - Кто угодно может запустить сервер. - No comment provided by engineer. - App build: %@ Сборка приложения: %@ @@ -1048,7 +1178,7 @@ swipe action App passcode is replaced with self-destruct passcode. - Код доступа в приложение будет заменен кодом самоуничтожения. + Код доступа в приложение будет заменён кодом самоуничтожения. No comment provided by engineer. @@ -1148,6 +1278,7 @@ swipe action Audio call + Аудиозвонок No comment provided by engineer. @@ -1182,7 +1313,7 @@ swipe action Auto-accept - Автоприем + Автоприём No comment provided by engineer. @@ -1192,7 +1323,7 @@ swipe action Auto-accept images - Автоприем изображений + Автоприём изображений No comment provided by engineer. @@ -1217,7 +1348,24 @@ swipe action Bad message hash - Ошибка хэш сообщения + Ошибка хэша сообщения + No comment provided by engineer. + + + Be free +in your network + Будь свободен +в своей сети + No comment provided by engineer. + + + Be free in your network. + Будь свободен в своей сети. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + Потому что мы разрушили саму возможность узнать, кто вы. Чтобы вашу свободу невозможно было отнять. No comment provided by engineer. @@ -1282,7 +1430,7 @@ swipe action Black - Черная + Чёрная No comment provided by engineer. @@ -1315,6 +1463,11 @@ swipe action Заблокировать члена группы? No comment provided by engineer. + + Block subscriber for all? + Заблокировать подписчика для всех? + No comment provided by engineer. + Blocked by admin Заблокирован администратором @@ -1365,6 +1518,16 @@ swipe action Вы и Ваш контакт можете отправлять голосовые сообщения. No comment provided by engineer. + + Bottom bar + Нижнее меню + No comment provided by engineer. + + + Broadcast + Опубликовать + compose placeholder for channel owner + 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)! @@ -1372,8 +1535,8 @@ swipe action Business address - Бизнес адрес - No comment provided by engineer. + Бизнес-адрес + chat link info line Business chats @@ -1382,7 +1545,7 @@ swipe action Business connection - Бизнес контакт + Бизнес-контакт No comment provided by engineer. @@ -1395,18 +1558,9 @@ swipe action По профилю чата или [по соединению](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА). No comment provided by engineer. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - Используя SimpleX Chat, Вы согласны: -- отправлять только законные сообщения в публичных группах. -- уважать других пользователей – не отправлять спам. - No comment provided by engineer. - Call already ended! - Звонок уже завершен! + Звонок уже завершён! No comment provided by engineer. @@ -1426,7 +1580,7 @@ swipe action Can't call contact - Не удается позвонить контакту + Не удаётся позвонить контакту No comment provided by engineer. @@ -1503,7 +1657,7 @@ new chat action Change chat profiles - Поменять профили + Изменить профили чата authentication reason @@ -1552,6 +1706,82 @@ new chat action authentication reason set passcode view + + Channel + Канал + No comment provided by engineer. + + + Channel display name + Имя канала + No comment provided by engineer. + + + Channel full name (optional) + Полное имя канала (необязательно) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + У канала нет активных релеев. Попробуйте подключиться позже. + alert message +alert subtitle + + + Channel image + Картинка канала + No comment provided by engineer. + + + Channel link + Ссылка канала + chat link info line + + + Channel preferences + Предпочтения канала + No comment provided by engineer. + + + Channel profile + Профиль канала + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + Профиль канала хранится на устройствах подписчиков и на чат-релеях. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + Профиль канала был изменен. Если Вы сохраните его, обновлённый профиль будет отправлен подписчикам канала. + alert message + + + Channel temporarily unavailable + Канал временно недоступен + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + Канал будет удалён для всех подписчиков - это нельзя отменить! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + Канал будет удалён для Вас - это нельзя отменить! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + Канал начнёт работу с %1$d из %2$d релеев. Продолжить? + alert message + + + Channels + Каналы + No comment provided by engineer. + Chat Разговор @@ -1609,7 +1839,7 @@ set passcode view Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. - Чат остановлен. Если вы уже использовали эту базу данных на другом устройстве, перенесите ее обратно до запуска чата. + Чат остановлен. Если вы уже использовали эту базу данных на другом устройстве, перенесите её обратно до запуска чата. No comment provided by engineer. @@ -1624,7 +1854,7 @@ set passcode view Chat preferences - Предпочтения + Настройки чатов No comment provided by engineer. @@ -1637,6 +1867,26 @@ set passcode view Профиль чата No comment provided by engineer. + + Chat relay + Чат-релей + No comment provided by engineer. + + + Chat relays + Чат-релеи + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + Чат-релеи пересылают сообщения в Ваших каналах. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + Чат-релеи пересылают сообщения подписчикам каналов. + No comment provided by engineer. + Chat theme Тема чата @@ -1644,18 +1894,19 @@ set passcode view Chat will be deleted for all members - this cannot be undone! - Разговор будет удален для всех участников - это действие нельзя отменить! + Разговор будет удалён для всех участников - это действие нельзя отменить! No comment provided by engineer. Chat will be deleted for you - this cannot be undone! - Разговор будет удален для Вас - это действие нельзя отменить! + Разговор будет удалён для Вас - это действие нельзя отменить! No comment provided by engineer. Chat with admins Чат с админами - chat toolbar + chat feature +chat toolbar Chat with member @@ -1664,7 +1915,7 @@ set passcode view Chat with members before they join. - Общайтесь с членами до того как принять их. + Общайтесь с членами группы до того как принять их. No comment provided by engineer. @@ -1672,11 +1923,26 @@ set passcode view Чаты No comment provided by engineer. + + Chats with admins are prohibited. + Чаты с админами запрещены. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + Чаты с админами в публичных каналах не имеют E2E шифрования - используйте только с доверенными чат-релеями. + alert message + Chats with members Чаты с членами группы No comment provided by engineer. + + Chats with members are disabled + Чаты с членами группы отключены + No comment provided by engineer. + Check messages every 20 min. Проверять сообщения каждые 20 минут. @@ -1687,6 +1953,16 @@ set passcode view Проверять сообщения по возможности. No comment provided by engineer. + + Check relay address and try again. + Проверьте адрес релея и попробуйте снова. + alert message + + + Check relay name and try again. + Проверьте имя релея и попробуйте снова. + alert message + Check server address and try again. Проверьте адрес сервера и попробуйте снова. @@ -1699,7 +1975,7 @@ set passcode view Choose _Migrate from another device_ on the new device and scan QR code. - Выберите _Мигрировать с другого устройства_ на новом устройстве и сосканируйте QR код. + Выберите _Мигрировать с другого устройства_ на новом устройстве и сосканируйте QR-код. No comment provided by engineer. @@ -1829,12 +2105,12 @@ set passcode view Configure ICE servers - Настройка ICE серверов + Настройка ICE-серверов No comment provided by engineer. - - Configure server operators - Настроить операторов серверов + + Configure relays + Настроить релеи No comment provided by engineer. @@ -1879,7 +2155,7 @@ set passcode view Confirm that you remember database passphrase to migrate it. - Подтвердите, что Вы помните пароль базы данных для ее миграции. + Подтвердите, что Вы помните пароль базы данных для её миграции. No comment provided by engineer. @@ -1895,7 +2171,8 @@ set passcode view Connect Соединиться - server test step + relay test step +server test step Connect automatically @@ -1941,6 +2218,11 @@ This is your own one-time link! Соединиться через ссылку new chat sheet title + + Connect via link or QR code + Соединитесь по ссылке или QR + No comment provided by engineer. + Connect via one-time link Соединиться через одноразовую ссылку @@ -2019,10 +2301,11 @@ This is your own one-time link! Connection error (AUTH) Ошибка соединения (AUTH) - No comment provided by engineer. + conn error description Connection failed + Ошибка соединения No comment provided by engineer. @@ -2077,6 +2360,11 @@ This is your own one-time link! Соединения No comment provided by engineer. + + Contact address + Адрес контакта + chat link info line + Contact allows Контакт разрешает @@ -2089,7 +2377,7 @@ This is your own one-time link! Contact deleted! - Контакт удален! + Контакт удалён! No comment provided by engineer. @@ -2104,7 +2392,7 @@ This is your own one-time link! Contact is deleted. - Контакт удален. + Контакт удалён. No comment provided by engineer. @@ -2124,7 +2412,7 @@ This is your own one-time link! Contact will be deleted - this cannot be undone! - Контакт будет удален — это нельзя отменить! + Контакт будет удалён - это нельзя отменить! No comment provided by engineer. @@ -2147,19 +2435,24 @@ This is your own one-time link! Продолжить No comment provided by engineer. + + Contribute + Внести свой вклад + No comment provided by engineer. + Conversation deleted! - Разговор удален! + Разговор удалён! No comment provided by engineer. Copy - Скопировать + Копировать No comment provided by engineer. Copy error - Ошибка копирования + Скопировать ошибку No comment provided by engineer. @@ -2175,12 +2468,7 @@ This is your own one-time link! Correct name to %@? Исправить имя на %@? - No comment provided by engineer. - - - Create - Создать - No comment provided by engineer. + alert message Create 1-time link @@ -2232,6 +2520,16 @@ This is your own one-time link! Создать профиль No comment provided by engineer. + + Create public channel + Создать публичный канал + No comment provided by engineer. + + + Create public channel (BETA) + Создать публичный канал (БЕТА) + No comment provided by engineer. + Create queue Создание очереди @@ -2242,11 +2540,21 @@ This is your own one-time link! Создайте Ваш адрес No comment provided by engineer. + + Create your link + Создайте Вашу ссылку + No comment provided by engineer. + Create your profile Создать профиль No comment provided by engineer. + + Create your public address + Создайте Ваш публичный адрес + No comment provided by engineer. + Created Создано @@ -2267,6 +2575,11 @@ This is your own one-time link! Создание ссылки на архив No comment provided by engineer. + + Creating channel + Создание канала + No comment provided by engineer. + Creating link… Создаётся ссылка… @@ -2350,7 +2663,7 @@ This is your own one-time link! Database encryption passphrase will be updated and stored in the keychain. - Пароль базы данных будет изменен и сохранен в Keychain. + Пароль базы данных будет изменен и сохранён в Keychain. No comment provided by engineer. @@ -2388,12 +2701,12 @@ This is your own one-time link! Database passphrase is different from saved in the keychain. - Пароль базы данных отличается от сохраненного в Keychain. + Пароль базы данных отличается от сохранённого в Keychain. No comment provided by engineer. Database passphrase is required to open chat. - Введите пароль базы данных чтобы открыть чат. + Введите пароль базы данных, чтобы открыть чат. No comment provided by engineer. @@ -2404,7 +2717,7 @@ This is your own one-time link! Database will be encrypted and the passphrase stored in the keychain. - База данных будет зашифрована и пароль сохранен в Keychain. + База данных будет зашифрована и пароль сохранён в Keychain. No comment provided by engineer. @@ -2425,10 +2738,10 @@ This is your own one-time link! Отладка доставки No comment provided by engineer. - - Decentralized - Децентрализованный - No comment provided by engineer. + + Decode link + Расшифровать ссылку + relay test step Decryption error @@ -2476,6 +2789,16 @@ swipe action Удалить и уведомить контакт No comment provided by engineer. + + Delete channel + Удалить канал + No comment provided by engineer. + + + Delete channel? + Удалить канал? + No comment provided by engineer. + Delete chat Удалить разговор @@ -2548,7 +2871,7 @@ swipe action Delete for everyone - Удалить для всех + Удаление для всех chat feature @@ -2588,15 +2911,17 @@ swipe action Delete member message? - Удалить сообщение участника? + Удалить сообщение члена группы\? No comment provided by engineer. Delete member messages + Удалить сообщения члена группы No comment provided by engineer. Delete member messages? + Удалить сообщения члена группы? alert title @@ -2645,6 +2970,11 @@ alert button Удаление очереди server test step + + Delete relay + Удалить релей + No comment provided by engineer. + Delete report Удалить сообщение о нарушении @@ -2802,14 +3132,24 @@ alert button Direct messages between members are prohibited in this chat. - Личные сообщения запрещены в этой группе. + Прямые сообщения между членами группы запрещены. No comment provided by engineer. Direct messages between members are prohibited. - Прямые сообщения между членами запрещены. + Прямые сообщения между членами группы запрещены. No comment provided by engineer. + + Direct messages between subscribers are prohibited. + Прямые сообщения между подписчиками запрещены. + No comment provided by engineer. + + + Disable + Выключить + alert button + Disable (keep overrides) Выключить (кроме исключений) @@ -2857,7 +3197,7 @@ alert button Disappearing messages are prohibited. - Исчезающие сообщения запрещены в этой группе. + Исчезающие сообщения запрещены. No comment provided by engineer. @@ -2872,7 +3212,7 @@ alert button Disconnect - Разрыв соединения + Отключить server test step @@ -2915,9 +3255,14 @@ alert button Не отправлять историю новым членам. No comment provided by engineer. + + Do not send history to new subscribers. + Не отправлять историю новым подписчикам. + No comment provided by engineer. + Do not use credentials with proxy. - Не использовать учетные данные с прокси. + Не использовать учётные данные с прокси. No comment provided by engineer. @@ -2963,7 +3308,7 @@ chat item action Download errors - Ошибки приема + Ошибки приёма No comment provided by engineer. @@ -3016,11 +3361,21 @@ chat item action E2E зашифрованные нотификации. No comment provided by engineer. + + Easier to invite your friends 👋 + Проще пригласить друзей 👋 + No comment provided by engineer. + Edit Редактировать chat item action + + Edit channel profile + Редактировать профиль канала + No comment provided by engineer. + Edit group profile Редактировать профиль группы @@ -3034,7 +3389,7 @@ chat item action Enable Включить - No comment provided by engineer. + alert button Enable (keep overrides) @@ -3056,6 +3411,11 @@ chat item action Включить TCP keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + Включите хотя бы один чат-релей в настройках Сеть и серверы. + channel creation warning + Enable automatic message deletion? Включить автоматическое удаление сообщений? @@ -3066,6 +3426,11 @@ chat item action Включить доступ к камере No comment provided by engineer. + + Enable chats with admins? + Включить чаты с админами? + alert title + Enable disappearing messages by default. Включите исчезающие сообщения по умолчанию. @@ -3086,16 +3451,16 @@ chat item action Включить мгновенные уведомления? No comment provided by engineer. + + Enable link previews? + Включить картинки ссылок? + alert title + Enable lock Включить блокировку No comment provided by engineer. - - Enable notifications - Включить уведомления - No comment provided by engineer. - Enable periodic notifications? Включить периодические уведомления? @@ -3138,7 +3503,7 @@ chat item action Encrypt stored files & media - Шифруйте сохраненные файлы и медиа + Шифруйте сохранённые файлы и медиа No comment provided by engineer. @@ -3173,7 +3538,7 @@ chat item action Encrypted message: no passphrase - Зашифрованное сообщение: пароль не сохранен + Зашифрованное сообщение: пароль не сохранён notification @@ -3201,6 +3566,11 @@ chat item action Введите Код No comment provided by engineer. + + Enter channel name… + Введите имя канала… + No comment provided by engineer. + Enter correct passphrase. Введите правильный пароль. @@ -3226,6 +3596,16 @@ chat item action Введите пароль выше, чтобы раскрыть! No comment provided by engineer. + + Enter profile name... + Введите имя профиля... + No comment provided by engineer. + + + Enter relay name… + Введите имя релея… + No comment provided by engineer. + Enter server manually Ввести сервер вручную @@ -3254,7 +3634,7 @@ chat item action Error Ошибка - No comment provided by engineer. + conn error description Error aborting address change @@ -3263,7 +3643,7 @@ chat item action Error accepting conditions - Ошибка приема условий + Ошибка приёма условий alert title @@ -3281,6 +3661,11 @@ chat item action Ошибка при добавлении членов группы No comment provided by engineer. + + Error adding relay + Ошибка добавления релея + alert title + Error adding server Ошибка добавления сервера @@ -3341,6 +3726,11 @@ chat item action Ошибка при создании адреса No comment provided by engineer. + + Error creating channel + Ошибка при создании канала + alert title + Error creating group Ошибка при создании группы @@ -3476,11 +3866,6 @@ chat item action Ошибка при открытии чата No comment provided by engineer. - - Error opening group - Ошибка при открытии группы - No comment provided by engineer. - Error receiving file Ошибка при получении файла @@ -3523,7 +3908,12 @@ chat item action Error saving ICE servers - Ошибка при сохранении ICE серверов + Ошибка при сохранении ICE-серверов + No comment provided by engineer. + + + Error saving channel profile + Ошибка при сохранении профиля канала No comment provided by engineer. @@ -3591,6 +3981,11 @@ chat item action Ошибка настроек отчётов о доставке! No comment provided by engineer. + + Error sharing channel + Ошибка при публикации канала + alert title + Error starting chat Ошибка при запуске чата @@ -3671,7 +4066,8 @@ snd error text Error: %@. Ошибка: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3760,7 +4156,7 @@ snd error text Faster joining and more reliable messages. - Быстрое вступление и надежная доставка сообщений. + Быстрое вступление и надёжная доставка сообщений. No comment provided by engineer. @@ -3799,7 +4195,7 @@ snd error text File not found - most likely file was deleted or cancelled. - Файл не найден - скорее всего, файл был удален или отменен. + Файл не найден - скорее всего, файл был удалён или отменен. file error text @@ -3859,7 +4255,7 @@ snd error text Files and media are prohibited. - Файлы и медиа запрещены в этой группе. + Файлы и медиа запрещены. No comment provided by engineer. @@ -3874,6 +4270,7 @@ snd error text Filter + Фильтр No comment provided by engineer. @@ -3898,7 +4295,7 @@ snd error text Find chats faster - Быстро найти чаты + Быстрый поиск чатов No comment provided by engineer. @@ -3913,8 +4310,9 @@ snd error text Fingerprint in server address does not match certificate. - Возможно, хэш сертификата в адресе сервера неверный. - server test error + Хэш в адресе сервера не соответствует сертификату. + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3948,7 +4346,7 @@ snd error text Fix not supported by group member - Починка не поддерживается членом группы. + Починка не поддерживается членом группы No comment provided by engineer. @@ -3956,10 +4354,16 @@ snd error text Для всех модераторов No comment provided by engineer. + + For anyone to reach you + Любой может связаться с Вами + No comment provided by engineer. + For chat profile %@: Для профиля чата %@: - servers error + servers error +servers warning For console @@ -4100,11 +4504,21 @@ Error: %2$@ ГИФ файлы и стикеры No comment provided by engineer. + + Get link + Получить ссылку + relay test step + Get notified when mentioned. Уведомления, когда Вас упомянули. No comment provided by engineer. + + Get started + Начать + No comment provided by engineer. + Good afternoon! Добрый день! @@ -4163,7 +4577,7 @@ Error: %2$@ Group link Ссылка группы - No comment provided by engineer. + chat link info line Group links @@ -4227,7 +4641,7 @@ Error: %2$@ Help admins moderating their groups. - Помогайте администраторам модерировать их группы. + Помогайте админам модерировать их группы. No comment provided by engineer. @@ -4247,7 +4661,7 @@ Error: %2$@ Hide - Спрятать + Скрыть chat item action @@ -4275,6 +4689,11 @@ Error: %2$@ История не отправляется новым членам. No comment provided by engineer. + + History is not sent to new subscribers. + История не отправляется новым подписчикам. + No comment provided by engineer. + How SimpleX works Как SimpleX работает @@ -4302,7 +4721,7 @@ Error: %2$@ How to use it - Про адрес + Как использовать No comment provided by engineer. @@ -4317,12 +4736,12 @@ Error: %2$@ ICE servers (one per line) - ICE серверы (один на строке) + ICE-серверы (один на строке) No comment provided by engineer. IP address - IP адрес + IP-адрес No comment provided by engineer. @@ -4342,6 +4761,7 @@ Error: %2$@ If you joined or created channels, they will stop working permanently. + Если Вы присоединились к каналам или создали их, они перестанут работать навсегда. down migration warning @@ -4366,6 +4786,7 @@ Error: %2$@ Images + Изображения No comment provided by engineer. @@ -4373,11 +4794,6 @@ Error: %2$@ Сразу No comment provided by engineer. - - Immune to spam - Защищен от спама - No comment provided by engineer. - Import Импортировать @@ -4471,7 +4887,7 @@ More improvements are coming soon! Incognito mode protects your privacy by using a new random profile for each contact. - Режим Инкогнито защищает Вашу конфиденциальность — для каждого контакта создается новый случайный профиль. + Режим Инкогнито защищает Вашу конфиденциальность - для каждого контакта создаётся новый случайный профиль. No comment provided by engineer. @@ -4519,9 +4935,9 @@ More improvements are coming soon! Роль при вступлении 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) + + Install SimpleX Chat for terminal + SimpleX Chat для терминала No comment provided by engineer. @@ -4573,13 +4989,13 @@ More improvements are coming soon! Invalid QR code - Неверный QR код + Ошибка QR-кода No comment provided by engineer. Invalid connection link Ошибка в ссылке контакта - No comment provided by engineer. + conn error description Invalid display name! @@ -4599,7 +5015,17 @@ More improvements are coming soon! Invalid name! Неверное имя! - No comment provided by engineer. + alert title + + + Invalid relay address! + Неверный адрес релея! + alert title + + + Invalid relay name! + Неверное имя релея! + alert title Invalid response @@ -4628,11 +5054,17 @@ More improvements are coming soon! Invite member + Пригласить члена группы No comment provided by engineer. Invite members - Пригласить членов группы + Пригласить в группу + No comment provided by engineer. + + + Invite someone privately + Пригласите конфиденциально No comment provided by engineer. @@ -4652,12 +5084,12 @@ More improvements are coming soon! Irreversible message deletion is prohibited in this chat. - Необратимое удаление сообщений запрещено в этом чате. + Необратимое удаление сообщений запрещено. No comment provided by engineer. Irreversible message deletion is prohibited. - Необратимое удаление сообщений запрещено в этой группе. + Необратимое удаление сообщений запрещено. No comment provided by engineer. @@ -4683,7 +5115,7 @@ More improvements are coming soon! It protects your IP address and connections. - Защищает ваш IP адрес и соединения. + Защищает ваш IP-адрес и соединения. No comment provided by engineer. @@ -4708,7 +5140,12 @@ More improvements are coming soon! Join as %@ - вступить как %@ + Вступить как %s + No comment provided by engineer. + + + Join channel + Вступить в канал No comment provided by engineer. @@ -4798,6 +5235,16 @@ This is your link for group %@! Выйти swipe action + + Leave channel + Покинуть канал + No comment provided by engineer. + + + Leave channel? + Выйти из канала? + No comment provided by engineer. + Leave chat Покинуть разговор @@ -4823,6 +5270,11 @@ This is your link for group %@! Меньше трафик в мобильных сетях. No comment provided by engineer. + + Let someone connect to you + Дайте собеседнику Вашу ссылку + No comment provided by engineer. + Let's talk in SimpleX Chat Давайте поговорим в SimpleX Chat @@ -4843,6 +5295,11 @@ This is your link for group %@! Свяжите мобильное и настольное приложения! 🔗 No comment provided by engineer. + + Link signature verified. + Подпись ссылки проверена. + owner verification + Linked desktop options Опции связанных компьютеров @@ -4855,6 +5312,7 @@ This is your link for group %@! Links + Ссылки No comment provided by engineer. @@ -4919,12 +5377,12 @@ This is your link for group %@! Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. - Пожалуйста, проверьте, что адреса WebRTC ICE серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется. + Пожалуйста, проверьте, что адреса WebRTC ICE-серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется. No comment provided by engineer. Mark deleted for everyone - Пометить как удаленное для всех + Пометить как удалённое для всех No comment provided by engineer. @@ -4974,7 +5432,7 @@ This is your link for group %@! Member inactive - Член неактивен + Член группы неактивен item status text @@ -4984,6 +5442,7 @@ This is your link for group %@! Member messages will be deleted - this cannot be undone! + Сообщения члена группы будут удалены - это нельзя отменить! alert message @@ -5008,17 +5467,17 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! - Член будет удален из разговора - это действие нельзя отменить! + Член будет удалён из разговора - это действие нельзя отменить! alert message Member will be removed from group - this cannot be undone! - Член группы будет удален - это действие нельзя отменить! + Член группы будет удалён - это действие нельзя отменить! alert message Member will join the group, accept member? - Участник хочет присоединиться к группе. Принять? + Член группы хочет присоединиться. Принять? alert message @@ -5026,6 +5485,11 @@ This is your link for group %@! Члены могут добавлять реакции на сообщения. No comment provided by engineer. + + Members can chat with admins. + Члены группы могут общаться с админами. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) Члены могут необратимо удалять отправленные сообщения. (24 часа) @@ -5038,12 +5502,12 @@ This is your link for group %@! Members can send SimpleX links. - Члены могут отправлять ссылки SimpleX. + Члены группы могут отправлять ссылки SimpleX. No comment provided by engineer. Members can send direct messages. - Члены могут посылать прямые сообщения. + Члены могут посылать личные сообщения. No comment provided by engineer. @@ -5058,12 +5522,12 @@ This is your link for group %@! Members can send voice messages. - Члены могут отправлять голосовые сообщения. + Члены группы могут отправлять голосовые сообщения. No comment provided by engineer. Mention members 👋 - Упоминайте участников 👋 + Упоминайте членов группы 👋 No comment provided by engineer. @@ -5078,7 +5542,7 @@ This is your link for group %@! Message delivery receipts! - Отчеты о доставке сообщений! + Отчёты о доставке сообщений! No comment provided by engineer. @@ -5091,6 +5555,11 @@ This is your link for group %@! Черновик сообщения No comment provided by engineer. + + Message error + Ошибка сообщения + No comment provided by engineer. + Message forwarded Сообщение переслано @@ -5123,12 +5592,12 @@ This is your link for group %@! Message reactions are prohibited. - Реакции на сообщения запрещены в этой группе. + Реакции на сообщения запрещены. No comment provided by engineer. Message reception - Прием сообщений + Приём сообщений No comment provided by engineer. @@ -5178,7 +5647,7 @@ This is your link for group %@! Messages are protected by **end-to-end encryption**. - Сообщения защищены **end-to-end шифрованием**. + Сообщения защищены **сквозным шифрованием**. No comment provided by engineer. @@ -5186,6 +5655,16 @@ This is your link for group %@! Сообщения от %@ будут показаны! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + Сообщения в этом канале **не защищены сквозным шифрованием**. Чат-релеи могут видеть эти сообщения. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + Сообщения в этом канале не защищены сквозным шифрованием. Чат-релеи могут видеть эти сообщения. + E2EE info chat item + Messages in this chat will never be deleted. Сообщения в этом чате никогда не будут удалены. @@ -5208,12 +5687,17 @@ This is your link for group %@! Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. - Сообщения, файлы и звонки защищены **end-to-end шифрованием** с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома. + Сообщения, файлы и звонки защищены **сквозным шифрованием** с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома. No comment provided by engineer. Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery. - Сообщения, файлы и звонки защищены **квантово-устойчивым end-to-end шифрованием** с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома. + Сообщения, файлы и звонки защищены **квантово-устойчивым сквозным шифрованием** с идеальной прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома. + No comment provided by engineer. + + + Migrate + Мигрировать No comment provided by engineer. @@ -5221,11 +5705,6 @@ This is your link for group %@! Мигрировать устройство No comment provided by engineer. - - Migrate from another device - Миграция с другого устройства - No comment provided by engineer. - Migrate here Мигрировать сюда @@ -5238,7 +5717,7 @@ This is your link for group %@! Migrate to another device via QR code. - Мигрируйте на другое устройство через QR код. + Мигрируйте на другое устройство через QR-код. No comment provided by engineer. @@ -5303,12 +5782,12 @@ This is your link for group %@! More reliable network connection. - Более надежное соединение с сетью. + Более надёжное соединение с сетью. No comment provided by engineer. More reliable notifications - Более надежные уведомления + Более надёжные уведомления No comment provided by engineer. @@ -5346,6 +5825,11 @@ This is your link for group %@! Сеть и серверы No comment provided by engineer. + + Network commitments + Обязательства сети + No comment provided by engineer. + Network connection Интернет-соединение @@ -5356,6 +5840,11 @@ This is your link for group %@! Децентрализация сети No comment provided by engineer. + + Network error + Ошибка сети + conn error description + Network issues - message expired after many attempts to send it. Ошибка сети - сообщение не было отправлено после многократных попыток. @@ -5371,6 +5860,13 @@ This is your link for group %@! Оператор сети No comment provided by engineer. + + Network routers cannot know +who talks to whom + Серверы сети не могут знать, +кто с кем общается + No comment provided by engineer. + Network settings Настройки сети @@ -5386,6 +5882,11 @@ This is your link for group %@! Новый token status text + + New 1-time link + Новая одноразовая ссылка + No comment provided by engineer. + New Passcode Новый Код @@ -5393,12 +5894,12 @@ This is your link for group %@! New SOCKS credentials will be used every time you start the app. - Новые учетные данные SOCKS будут использоваться при каждом запуске приложения. + Новые учётные данные SOCKS будут использоваться при каждом запуске приложения. No comment provided by engineer. New SOCKS credentials will be used for each server. - Новые учетные данные SOCKS будут использоваться для каждого сервера. + Новые учётные данные SOCKS будут использоваться для каждого сервера. No comment provided by engineer. @@ -5411,6 +5912,11 @@ This is your link for group %@! Новый интерфейс 🎉 No comment provided by engineer. + + New chat relay + Новый чат-релей + No comment provided by engineer. + New contact request Новый запрос на соединение @@ -5458,7 +5964,7 @@ This is your link for group %@! New member wants to join the group. - Новый участник хочет присоединиться к группе. + Новый член группы хочет присоединиться. rcv group event chat item @@ -5481,11 +5987,33 @@ This is your link for group %@! Нет No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + Без аккаунта. Без номера. Без email. Без ID. +Самое безопасное шифрование. + No comment provided by engineer. + + + No active relays + Нет активных релеев + No comment provided by engineer. + No app password Нет кода доступа Authentication unavailable + + No chat relays + Нет чат-релеев + No comment provided by engineer. + + + No chat relays enabled. + Чат-релеи не включены. + servers warning + No chats Нет чатов @@ -5608,12 +6136,12 @@ This is your link for group %@! No servers to receive files. - Нет серверов для приема файлов. + Нет серверов для приёма файлов. servers error No servers to receive messages. - Нет серверов для приема сообщений. + Нет серверов для приёма сообщений. servers error @@ -5631,11 +6159,26 @@ This is your link for group %@! Нет непрочитанных чатов No comment provided by engineer. - - No user identifiers. - Без идентификаторов пользователей. + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. + Никто не отслеживал ваши разговоры. Никто не составлял карту ваших перемещений. Конфиденциальность не была функцией - это был образ жизни. No comment provided by engineer. + + Non-profit governance + Некоммерческое управление + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + Не более надёжный замок на чужой двери. Не более вежливый хозяин, который уважает вашу частную жизнь, но всё равно ведёт учёт всех посетителей. Вы не гость. Вы у себя дома. Ни один король не войдёт в ваш дом - вы суверенны. + No comment provided by engineer. + + + Not all relays connected + Не все релеи подключены + alert title + Not compatible! Несовместимая версия! @@ -5693,11 +6236,11 @@ This is your link for group %@! OK OK - No comment provided by engineer. + alert button Off - Выключено + Нет blur media @@ -5712,11 +6255,21 @@ new chat action Предыдущая версия данных чата No comment provided by engineer. + + On your phone, not on servers. + На Вашем телефоне, не на серверах. + No comment provided by engineer. + One-time invitation link Одноразовая ссылка No comment provided by engineer. + + One-time link + Одноразовая ссылка + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5736,6 +6289,11 @@ Requires compatible VPN. Onion хосты не используются. No comment provided by engineer. + + Only channel owners can change channel preferences. + Изменить настройки канала могут только владельцы канала. + No comment provided by engineer. + Only chat owners can change preferences. Только владельцы разговора могут поменять предпочтения. @@ -5839,7 +6397,8 @@ Requires compatible VPN. Open Открыть - alert action + alert action +alert button Open Settings @@ -5851,6 +6410,11 @@ Requires compatible VPN. Открыть изменения No comment provided by engineer. + + Open channel + Открыть канал + new chat action + Open chat Открыть чат @@ -5871,6 +6435,11 @@ Requires compatible VPN. Открыть условия No comment provided by engineer. + + Open external link? + Открыть внешнюю ссылку? + alert title + Open full link Открыть полную ссылку @@ -5891,6 +6460,11 @@ Requires compatible VPN. Открытие миграции на другое устройство authentication reason + + Open new channel + Открыть новый канал + new chat action + Open new chat Открыть новый чат @@ -5936,6 +6510,17 @@ Requires compatible VPN. Сервер оператора alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + Операторы обязуются: +- Быть независимыми +- Минимизировать использование метаданных +- Использовать проверенный и открытый исходный код + No comment provided by engineer. + Or import archive file Или импортировать файл архива @@ -5948,7 +6533,7 @@ Requires compatible VPN. Or scan QR code - Или отсканируйте QR код + Или отсканируйте QR-код No comment provided by engineer. @@ -5956,6 +6541,11 @@ Requires compatible VPN. Или передайте эту ссылку No comment provided by engineer. + + Or show QR in person or via video call. + Или покажите QR лично или через видеозвонок. + No comment provided by engineer. + Or show this code Или покажите этот код @@ -5966,6 +6556,11 @@ Requires compatible VPN. Или поделиться конфиденциально No comment provided by engineer. + + Or use this QR - print or show online. + Или используйте этот QR - распечатайте или покажите онлайн. + No comment provided by engineer. + Organize chats into lists Организуйте чаты в списки @@ -5983,6 +6578,21 @@ Requires compatible VPN. %@ alert message + + Owner + Владелец + No comment provided by engineer. + + + Owners + Владельцы + No comment provided by engineer. + + + Ownership: you can run your own relays. + Владение: Вы можете запустить свои собственные релеи. + No comment provided by engineer. + PING count Количество PING @@ -6038,6 +6648,11 @@ Requires compatible VPN. Вставить изображение No comment provided by engineer. + + Paste link / Scan + Вставить ссылку / Сканировать + No comment provided by engineer. + Paste link to connect! Вставьте ссылку, чтобы соединиться! @@ -6092,12 +6707,12 @@ Please share any other issues with the developers. 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. - Пожалуйста, проверьте Ваше соединение с %@ и попробуйте еще раз. + Пожалуйста, проверьте Ваше соединение с %@ и попробуйте ещё раз. alert message @@ -6139,7 +6754,7 @@ Error: %@ Please report it to the developers. - Пожалуйста, сообщите об этой ошибке девелоперам. + Пожалуйста, сообщите об этой ошибке разработчикам. No comment provided by engineer. @@ -6149,12 +6764,12 @@ Error: %@ 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. @@ -6192,6 +6807,16 @@ Error: %@ Сохранить последний черновик, вместе с вложениями. No comment provided by engineer. + + Preset relay address + Адрес релея по умолчанию + No comment provided by engineer. + + + Preset relay name + Имя релея по умолчанию + No comment provided by engineer. + Preset server address Адрес сервера по умолчанию @@ -6227,19 +6852,19 @@ Error: %@ Политика конфиденциальности и условия использования. No comment provided by engineer. - - Privacy redefined - Более конфиденциальный + + Privacy: for owners and subscribers. + Конфиденциальность: для владельцев и подписчиков. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. - Частные разговоры, группы и Ваши контакты недоступны для операторов серверов. + + Private and secure messaging. + Конфиденциальный и безопасный обмен сообщениями. No comment provided by engineer. Private filenames - Защищенные имена файлов + Защищённые имена файлов No comment provided by engineer. @@ -6277,6 +6902,11 @@ Error: %@ Таймаут конфиденциальной доставки alert title + + Proceed + Продолжить + alert action + Profile and server connections Профиль и соединения на сервере @@ -6302,9 +6932,9 @@ Error: %@ Тема профиля No comment provided by engineer. - - Profile update will be sent to your contacts. - Обновлённый профиль будет отправлен Вашим контактам. + + Profile update will be sent to your SimpleX contacts. + Обновление профиля будет отправлено Вашим SimpleX контактам. alert message @@ -6312,6 +6942,11 @@ Error: %@ Запретить аудио/видео звонки. No comment provided by engineer. + + Prohibit chats with admins. + Запретить чаты с админами. + No comment provided by engineer. + Prohibit irreversible message deletion. Запретить необратимое удаление сообщений. @@ -6339,27 +6974,32 @@ Error: %@ Prohibit sending direct messages to members. - Запретить посылать прямые сообщения членам группы. + Запретить посылать личные сообщения членам группы. + No comment provided by engineer. + + + Prohibit sending direct messages to subscribers. + Запретить отправку личных сообщений подписчикам. 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 IP address - Защитить IP адрес + Защитить IP-адрес No comment provided by engineer. @@ -6370,7 +7010,7 @@ Error: %@ Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings. - Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами. + Защитите ваш IP-адрес от серверов сообщений, выбранных Вашими контактами. Включите в настройках *Сети и серверов*. No comment provided by engineer. @@ -6409,6 +7049,11 @@ Enable in *Network & servers* settings. Прокси требует пароль No comment provided by engineer. + + Public channels - speak freely 🚀 + Публичные каналы - говорите свободно 🚀 + No comment provided by engineer. + Push notifications Доставка уведомлений @@ -6449,24 +7094,14 @@ Enable in *Network & servers* settings. Узнать больше No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - Дополнительная информация в [Руководстве пользователя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + Узнать больше в Руководстве пользователя. No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - 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). + + Read more in our GitHub repository. + Узнайте больше из нашего GitHub репозитория. No comment provided by engineer. @@ -6476,7 +7111,7 @@ Enable in *Network & servers* settings. Receive errors - Ошибки приема + Ошибки приёма No comment provided by engineer. @@ -6623,17 +7258,52 @@ swipe action Reject member? - Отклонить участника? + Отклонить члена группы? alert title + + Relay + Релей + No comment provided by engineer. + + + Relay address + Адрес релея + alert title + + + Relay connection failed + Ошибка подключения релея + alert title + + + Relay link + Ссылка релея + No comment provided by engineer. + + + Relay results: + Результаты релея: + alert message + Relay server is only used if necessary. Another party can observe your IP address. - Relay сервер используется только при необходимости. Другая сторона может видеть Ваш IP адрес. + Релей-сервер используется только при необходимости. Другая сторона может видеть Ваш IP-адрес. No comment provided by engineer. Relay server protects your IP address, but it can observe the duration of the call. - Relay сервер защищает Ваш IP адрес, но может отслеживать продолжительность звонка. + Релей-сервер защищает Ваш IP-адрес, но может отслеживать продолжительность звонка. + No comment provided by engineer. + + + Relay test failed! + Тест релея не пройден! + No comment provided by engineer. + + + Reliability: many relays per channel. + Надёжность: несколько релеев на каждый канал. No comment provided by engineer. @@ -6643,6 +7313,7 @@ swipe action Remove and delete messages + Удалить вместе с сообщениями alert action @@ -6675,6 +7346,16 @@ swipe action Удалить пароль из Keychain? No comment provided by engineer. + + Remove subscriber + Удалить подписчика + No comment provided by engineer. + + + Remove subscriber? + Удалить подписчика? + alert title + Removes messages and blocks members. Может удалять сообщения и блокировать членов. @@ -6817,12 +7498,12 @@ swipe action 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. @@ -6867,7 +7548,7 @@ swipe action Review members - Одобрять членов + Одобрять членов группы admission stage @@ -6902,12 +7583,17 @@ swipe action SMP server - SMP сервер + SMP-сервер No comment provided by engineer. SOCKS proxy - SOCKS прокси + SOCKS-прокси + No comment provided by engineer. + + + Safe web links + Безопасные веб-ссылки No comment provided by engineer. @@ -6936,6 +7622,11 @@ chat item action Сохранить (и уведомить членов) alert button + + Save (and notify subscribers) + Сохранить (и уведомить подписчиков) + alert button + Save admission settings? Сохранить настройки вступления? @@ -6951,6 +7642,11 @@ chat item action Сохранить и уведомить членов группы No comment provided by engineer. + + Save and notify subscribers + Сохранить и уведомить подписчиков + No comment provided by engineer. + Save and reconnect Сохранить и переподключиться @@ -6961,6 +7657,16 @@ chat item action Сохранить сообщение и обновить группу No comment provided by engineer. + + Save channel profile + Сохранить профиль канала + No comment provided by engineer. + + + Save channel profile? + Сохранить профиль канала? + alert title + Save group profile Сохранить профиль группы @@ -7023,7 +7729,7 @@ chat item action Saved WebRTC ICE servers will be removed - Сохраненные WebRTC ICE серверы будут удалены + Сохранённые WebRTC ICE-серверы будут удалены No comment provided by engineer. @@ -7033,7 +7739,7 @@ chat item action Saved message - Сохраненное сообщение + Сохранённое сообщение message info title @@ -7053,12 +7759,12 @@ chat item action Scan QR code - Сканировать QR код + Сканировать QR-код No comment provided by engineer. Scan QR code from desktop - Сканировать QR код с компьютера + Сканировать QR-код с компьютера No comment provided by engineer. @@ -7073,7 +7779,7 @@ chat item action Scan server QR code - Сканировать QR код сервера + Сканировать QR-код сервера No comment provided by engineer. @@ -7088,27 +7794,32 @@ chat item action Search files + Поиск файлов No comment provided by engineer. Search images + Поиск изображений No comment provided by engineer. Search links + Поиск ссылок No comment provided by engineer. Search or paste SimpleX link - Искать или вставьте ссылку SimpleX + Искать или вставить ссылку SimpleX No comment provided by engineer. Search videos + Поиск видео No comment provided by engineer. Search voice messages + Поиск голосовых сообщений No comment provided by engineer. @@ -7136,6 +7847,11 @@ chat item action Код безопасности No comment provided by engineer. + + Security: owners hold channel keys. + Безопасность: владельцы хранят ключи канала. + No comment provided by engineer. + Select Выбрать @@ -7183,7 +7899,7 @@ chat item action Send a live message - it will update for the recipient(s) as you type it - Отправить живое сообщение — оно будет обновляться для получателей по мере того, как Вы его вводите + Отправить живое сообщение - оно будет обновляться для получателей по мере того, как Вы его вводите No comment provided by engineer. @@ -7198,7 +7914,7 @@ chat item action Send direct message to connect - Отправьте сообщение чтобы соединиться + Отправить личное сообщение контакту No comment provided by engineer. @@ -7228,7 +7944,7 @@ chat item action Send messages directly when IP address is protected and your or destination server does not support private routing. - Отправлять сообщения напрямую, когда IP адрес защищен, и Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку. + Отправлять сообщения напрямую, когда IP-адрес защищён, и Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку. No comment provided by engineer. @@ -7248,7 +7964,7 @@ chat item action Send questions and ideas - Отправьте вопросы и идеи + Вопросы и предложения No comment provided by engineer. @@ -7266,6 +7982,11 @@ chat item action Отправить запрос без сообщения No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + Отправьте ссылку через любой мессенджер - это безопасно. Попросите вставить её в SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. Отправьте из галереи или из дополнительных клавиатур. @@ -7276,6 +7997,11 @@ chat item action Отправить до 100 последних сообщений новым членам. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + Отправлять до 100 последних сообщений новым подписчикам. + No comment provided by engineer. + Send your private feedback to groups. Отправляйте Ваши конфиденциальные предложения группе. @@ -7291,6 +8017,11 @@ chat item action Отправитель мог удалить запрос на соединение. No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + Отправка картинки ссылки может раскрыть Ваш IP-адрес веб-сайту. Вы можете изменить это в настройках безопасности позже. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. Отправка отчётов о доставке будет включена для всех контактов во всех видимых профилях чата. @@ -7403,7 +8134,7 @@ chat item action Server operator changed. - Оператор серверов изменен. + Оператор сервера изменен. alert title @@ -7416,6 +8147,11 @@ chat item action Протокол сервера изменен. alert title + + Server requires authorization to connect to relay, check password. + Для подключения к релею требуется авторизация, проверьте пароль. + relay test error + Server requires authorization to create queues, check password. Сервер требует авторизации для создания очередей, проверьте пароль. @@ -7546,6 +8282,16 @@ chat item action Настройки были изменены. alert message + + Setup notifications + Настроить уведомления + No comment provided by engineer. + + + Setup routers + Настроить серверы + No comment provided by engineer. + Shape profile images Форма картинок профилей @@ -7582,11 +8328,16 @@ chat item action Поделитесь адресом No comment provided by engineer. - - Share address with contacts? - Поделиться адресом с контактами? + + Share address with SimpleX contacts? + Поделиться адресом с контактами SimpleX? alert title + + Share channel + Поделиться каналом + No comment provided by engineer. + Share from other apps. Поделитесь из других приложений. @@ -7612,6 +8363,11 @@ chat item action Поделиться профилем No comment provided by engineer. + + Share relay address + Поделиться адресом релея + No comment provided by engineer. + Share this 1-time invite link Поделиться одноразовой ссылкой-приглашением @@ -7622,9 +8378,14 @@ chat item action Поделиться в SimpleX No comment provided by engineer. - - Share with contacts - Поделиться с контактами + + Share via chat + Поделиться в чате + No comment provided by engineer. + + + Share with SimpleX contacts + Поделиться с контактами SimpleX No comment provided by engineer. @@ -7649,7 +8410,7 @@ chat item action Show QR code - Показать QR код + Показать QR-код No comment provided by engineer. @@ -7659,7 +8420,7 @@ chat item action Show developer options - Показать опции для девелоперов + Показать опции для разработчиков No comment provided by engineer. @@ -7749,7 +8510,7 @@ chat item action SimpleX address settings - Настройки автоприема + Настройки автоприёма alert title @@ -7774,7 +8535,7 @@ chat item action SimpleX links - SimpleX ссылки + Ссылки SimpleX chat feature @@ -7797,14 +8558,14 @@ chat item action Аудит SimpleX протоколов от Trail of Bits. No comment provided by engineer. - - SimpleX relay link - Ссылка SimpleX relay + + SimpleX relay address + Адрес релея SimpleX simplex link type Simplified incognito mode - Упрощенный режим Инкогнито + Упрощённый режим Инкогнито No comment provided by engineer. @@ -7875,6 +8636,11 @@ report reason Квадрат, круг и все, что между ними. No comment provided by engineer. + + Star on GitHub + Поставить звёздочку на GitHub + No comment provided by engineer. + Start chat Запустить чат @@ -7975,6 +8741,78 @@ report reason Подписано No comment provided by engineer. + + Subscriber + Подписчик + No comment provided by engineer. + + + Subscriber reports + Сообщения о нарушениях + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + Подписчик будет удалён из канала - это нельзя отменить! + alert message + + + Subscribers + Подписчики + No comment provided by engineer. + + + Subscribers can add message reactions. + Подписчики могут добавлять реакции на сообщения. + No comment provided by engineer. + + + Subscribers can chat with admins. + Подписчики могут общаться с админами. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + Подписчики могут необратимо удалять отправленные сообщения. (24 часа) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + Подписчики могут отправлять сообщения о нарушениях модераторам. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + Подписчики могут отправлять ссылки SimpleX. + No comment provided by engineer. + + + Subscribers can send direct messages. + Подписчики могут отправлять личные сообщения. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + Подписчики могут отправлять исчезающие сообщения. + No comment provided by engineer. + + + Subscribers can send files and media. + Подписчики могут отправлять файлы и медиа. + No comment provided by engineer. + + + Subscribers can send voice messages. + Подписчики могут отправлять голосовые сообщения. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + Подписчики используют ссылку релея для подключения к каналу. +Адрес релея был использован для настройки этого релея для канала. + No comment provided by engineer. + Subscription errors Ошибки подписки @@ -8022,7 +8860,7 @@ report reason TCP connection timeout - Таймаут TCP соединения + Таймаут TCP-соединения No comment provided by engineer. @@ -8055,6 +8893,11 @@ report reason Сделать фото No comment provided by engineer. + + Talk to someone + Начните разговор + No comment provided by engineer. + Tap Connect to chat Нажмите Соединиться @@ -8070,9 +8913,9 @@ report reason Нажмите Соединиться, чтобы использовать бот No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. - Нажмите Создать адрес SimpleX в меню, чтобы создать его позже. + + Tap Join channel + Нажмите Войти в канал No comment provided by engineer. @@ -8087,12 +8930,12 @@ report reason Tap to Connect - Нажмите чтобы соединиться + Нажмите, чтобы соединиться No comment provided by engineer. Tap to activate profile. - Нажмите, чтобы сделать профиль активным. + Нажмите на профиль, чтобы переключиться. No comment provided by engineer. @@ -8105,6 +8948,11 @@ report reason Нажмите, чтобы вступить инкогнито No comment provided by engineer. + + Tap to open + Нажмите, чтобы открыть + No comment provided by engineer. + Tap to paste link Нажмите, чтобы вставить ссылку @@ -8123,13 +8971,19 @@ report reason Test failed at step %@. Ошибка теста на шаге %@. - server test failure + relay test failure +server test failure Test notifications Протестировать уведомления No comment provided by engineer. + + Test relay + Тест релея + No comment provided by engineer. + Test server Тестировать сервер @@ -8182,6 +9036,11 @@ It can happen because of some bug or when the connection is compromised.Приложение улучшает конфиденциальность используя разных операторов в каждом разговоре. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + Приложение удалило это сообщение после %lld попыток его получить. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Приложение будет запрашивать подтверждение загрузки с неизвестных серверов (за исключением .onion адресов). @@ -8194,9 +9053,14 @@ It can happen because of some bug or when the connection is compromised. The code you scanned is not a SimpleX link QR code. - Этот QR код не является SimpleX-ccылкой. + Этот QR-код не является SimpleX-ccылкой. No comment provided by engineer. + + The connection reached the limit of undelivered messages + Соединение достигло лимита недоставленных сообщений + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. Соединение достигло предела недоставленных сообщений. Возможно, Ваш контакт не в сети. @@ -8222,9 +9086,11 @@ It can happen because of some bug or when the connection is compromised.Шифрование работает, и новое соглашение не требуется. Это может привести к ошибкам соединения! No comment provided by engineer. - - The future of messaging - Будущее коммуникаций + + The first network where you own +your contacts and groups. + Первая сеть, в которой Вы владеете +своими контактами и группами. No comment provided by engineer. @@ -8244,7 +9110,7 @@ It can happen because of some bug or when the connection is compromised. The message will be marked as moderated for all members. - Сообщение будет помечено как удаленное для всех членов группы. + Сообщение будет помечено как удалённое для всех членов группы. No comment provided by engineer. @@ -8254,7 +9120,7 @@ It can happen because of some bug or when the connection is compromised. The messages will be marked as moderated for all members. - Сообщения будут помечены как удаленные для всех членов группы. + Сообщения будут помечены как удалённые для всех членов группы. No comment provided by engineer. @@ -8262,9 +9128,14 @@ It can happen because of some bug or when the connection is compromised.Предыдущая версия данных чата не удалена при перемещении, её можно удалить. No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + Древнейшая человеческая свобода - говорить с другим человеком без слежки - построенная на инфраструктуре, которая не может её предать. + No comment provided by engineer. + The same conditions will apply to operator **%@**. - Те же самые условия будут приняты для оператора **%@**. + Те же условия будут действовать для оператора **%s**. No comment provided by engineer. @@ -8299,7 +9170,7 @@ It can happen because of some bug or when the connection is compromised. The uploaded database archive will be permanently removed from the servers. - Загруженный архив базы данных будет навсегда удален с серверов. + Загруженный архив базы данных будет навсегда удалён с серверов. No comment provided by engineer. @@ -8307,6 +9178,16 @@ It can happen because of some bug or when the connection is compromised.Темы No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + Потом мы вышли в интернет, и каждая платформа попросила частичку вас - ваше имя, ваш номер, ваших друзей. Мы смирились с тем, что за возможность общаться приходится отдавать информацию о том, с кем мы общаемся. Каждое поколение людей и технологий жило так - телефон, электронная почта, мессенджеры, социальные сети. Казалось, что другого пути нет. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + Другой путь есть. Сеть без номеров телефонов. Без имён пользователей. Без аккаунтов. Без каких-либо идентификаторов пользователей. Сеть, которая соединяет людей и передаёт зашифрованные сообщения, не зная, кто с кем связан. + No comment provided by engineer. + These conditions will also apply for: **%@**. Эти условия также будут применены к: **%@**. @@ -8319,17 +9200,17 @@ It can happen because of some bug or when the connection is compromised. 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. @@ -8339,17 +9220,17 @@ It can happen because of some bug or when the connection is compromised. This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. - Это действие нельзя отменить — Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. + Это действие нельзя отменить - Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. No comment provided by engineer. This chat is protected by end-to-end encryption. - Чат защищен end-to-end шифрованием. + Чат защищён сквозным шифрованием. E2EE info chat item This chat is protected by quantum resistant end-to-end encryption. - Чат защищен квантово-устойчивым end-to-end шифрованием. + Чат защищён квантово-устойчивым сквозным шифрованием. E2EE info chat item @@ -8372,6 +9253,16 @@ It can happen because of some bug or when the connection is compromised.Эта группа больше не существует. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + Это адрес чат-релея, с ним нельзя соединиться. + alert message + + + This is your link for channel %@! + Это ваша ссылка на канал %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Эта ссылка требует новую версию. Обновите приложение или попросите Ваш контакт прислать совместимую ссылку. @@ -8384,7 +9275,7 @@ It can happen because of some bug or when the connection is compromised. This message was deleted or not received yet. - Это сообщение было удалено или еще не получено. + Это сообщение было удалено или ещё не получено. No comment provided by engineer. @@ -8422,6 +9313,11 @@ It can happen because of some bug or when the connection is compromised.Чтобы скрыть нежелательные сообщения. No comment provided by engineer. + + To make SimpleX Network last. + Чтобы сохранить сеть SimpleX для всех. + No comment provided by engineer. + To make a new connection Чтобы соединиться @@ -8439,7 +9335,7 @@ It can happen because of some bug or when the connection is compromised. To protect your IP address, private routing uses your SMP servers to deliver messages. - Чтобы защитить ваш IP адрес, приложение использует Ваши SMP серверы для конфиденциальной доставки сообщений. + Чтобы защитить Ваш IP-адрес, приложение использует Ваши SMP-серверы для конфиденциальной доставки сообщений. No comment provided by engineer. @@ -8451,7 +9347,7 @@ You will be prompted to complete authentication before this feature is enabled.< To protect your privacy, SimpleX uses separate IDs for each of your contacts. - Чтобы защитить Вашу конфиденциальность, SimpleX использует разные идентификаторы для каждого Вашeго контакта. + Чтобы защитить Вашу конфиденциальность, SimpleX использует разные ID для каждого Вашего контакта. No comment provided by engineer. @@ -8506,12 +9402,7 @@ You will be prompted to complete authentication before this feature is enabled.< 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 chat list: - Переключите список чатов: + Чтобы подтвердить безопасность сквозного шифрования с Вашим контактом сравните (или сканируйте) код на ваших устройствах. No comment provided by engineer. @@ -8529,6 +9420,11 @@ You will be prompted to complete authentication before this feature is enabled.< Прозрачность тулбара No comment provided by engineer. + + Top bar + Верхнее меню + No comment provided by engineer. + Total Всего @@ -8536,7 +9432,7 @@ You will be prompted to complete authentication before this feature is enabled.< Transport isolation - Отдельные сессии для + Отдельные транспортные сессии No comment provided by engineer. @@ -8546,7 +9442,7 @@ You will be prompted to complete authentication before this feature is enabled.< Trying to connect to the server used to receive messages from this connection. - Попытка подключиться к серверу, используемому для получения сообщений от этого соединения. + Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта. subscription status explanation @@ -8594,6 +9490,11 @@ You will be prompted to complete authentication before this feature is enabled.< Разблокировать члена группы? No comment provided by engineer. + + Unblock subscriber for all? + Разблокировать подписчика для всех? + No comment provided by engineer. + Undelivered messages Недоставленные сообщения @@ -8658,7 +9559,7 @@ You will be prompted to complete authentication before this feature is enabled.< 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. Возможно, Ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом. -Чтобы установить соединение, попросите Ваш контакт создать еще одну ссылку и проверьте Ваше соединение с сетью. +Чтобы установить соединение, попросите Ваш контакт создать ещё одну ссылку и проверьте Ваше соединение с сетью. No comment provided by engineer. @@ -8694,13 +9595,18 @@ To connect, please ask your contact to create another connection link and check Unsupported connection link Ссылка не поддерживается - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. До 100 последних сообщений отправляются новым членам. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + До 100 последних сообщений отправляется новым подписчикам. + No comment provided by engineer. + Update Обновить @@ -8723,12 +9629,12 @@ To connect, please ask your contact to create another connection link and check Updated conditions - Обновленные условия + Обновлённые условия No comment provided by engineer. Updating settings will re-connect the client to all servers. - Обновление настроек приведет к сбросу и установке нового соединения со всеми серверами. + Обновление настроек приведёт к сбросу и установке нового соединения со всеми серверами. No comment provided by engineer. @@ -8808,7 +9714,7 @@ To connect, please ask your contact to create another connection link and check Use SOCKS proxy - Использовать SOCKS прокси + Использовать SOCKS-прокси No comment provided by engineer. @@ -8826,11 +9732,6 @@ To connect, please ask your contact to create another connection link and check Использовать TCP-порт 443 только для серверов по умолчанию. No comment provided by engineer. - - Use chat - Использовать чат - No comment provided by engineer. - Use current profile Использовать активный профиль @@ -8846,6 +9747,11 @@ To connect, please ask your contact to create another connection link and check Использовать для сообщений No comment provided by engineer. + + Use for new channels + Использовать для новых каналов + No comment provided by engineer. + Use for new connections Использовать для новых соединений @@ -8868,7 +9774,7 @@ To connect, please ask your contact to create another connection link and check Use new incognito profile - Использовать новый Инкогнито профиль + Использовать новый профиль инкогнито new chat action @@ -8878,7 +9784,7 @@ To connect, please ask your contact to create another connection link and check Use private routing with unknown servers when IP address is not protected. - Использовать конфиденциальную доставку с неизвестными серверами, когда IP адрес не защищен. + Использовать конфиденциальную доставку с неизвестными серверами, когда IP-адрес не защищён. No comment provided by engineer. @@ -8886,6 +9792,11 @@ To connect, please ask your contact to create another connection link and check Использовать конфиденциальную доставку с неизвестными серверами. No comment provided by engineer. + + Use relay + Использовать релей + No comment provided by engineer. + Use server Использовать сервер @@ -8906,6 +9817,11 @@ To connect, please ask your contact to create another connection link and check Используйте приложение одной рукой. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + Используйте этот адрес в профиле социальных сетей, на сайте или в подписи email. + No comment provided by engineer. + Use web port Использовать веб-порт @@ -8926,6 +9842,11 @@ To connect, please ask your contact to create another connection link and check Используются серверы, предоставленные SimpleX Chat. No comment provided by engineer. + + Verify + Проверить + relay test step + Verify code with desktop Сверьте код с компьютером @@ -8988,6 +9909,7 @@ To connect, please ask your contact to create another connection link and check Videos + Видео No comment provided by engineer. @@ -9045,6 +9967,21 @@ To connect, please ask your contact to create another connection link and check Голосовое сообщение… No comment provided by engineer. + + Wait + Подождать + alert action + + + Wait response + Ожидание ответа + relay test step + + + Waiting for channel owner to add relays. + Ожидает, когда владелец канала добавит релеи. + No comment provided by engineer. + Waiting for desktop... Ожидается подключение компьютера... @@ -9052,12 +9989,12 @@ To connect, please ask your contact to create another connection link and check Waiting for file - Ожидается прием файла + Ожидается приём файла No comment provided by engineer. Waiting for image - Ожидается прием изображения + Ожидается приём изображения No comment provided by engineer. @@ -9077,17 +10014,22 @@ To connect, please ask your contact to create another connection link and check Warning: starting chat on multiple devices is not supported and will cause message delivery failures - Внимание: запуск чата на нескольких устройствах не поддерживается и приведет к сбоям доставки сообщений + Внимание: запуск чата на нескольких устройствах не поддерживается и приведёт к сбоям доставки сообщений No comment provided by engineer. Warning: you may lose some data! - Предупреждение: Вы можете потерять какие то данные! + Предупреждение: Вы можете потерять некоторые данные! + No comment provided by engineer. + + + We made connecting simpler for new users. + Мы упростили подключение для новых пользователей. No comment provided by engineer. WebRTC ICE servers - WebRTC ICE серверы + WebRTC ICE-серверы No comment provided by engineer. @@ -9112,7 +10054,7 @@ To connect, please ask your contact to create another connection link and check What's new - Новые функции + Что нового No comment provided by engineer. @@ -9132,7 +10074,12 @@ To connect, please ask your contact to create another connection link and check 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. + + + Why SimpleX is built. + Зачем создан SimpleX. No comment provided by engineer. @@ -9167,12 +10114,12 @@ To connect, please ask your contact to create another connection link and check Without Tor or VPN, your IP address will be visible to file servers. - Без Тора или ВПН, Ваш IP адрес будет доступен серверам файлов. + Без Tor или VPN, Ваш IP-адрес будет доступен серверам файлов. No comment provided by engineer. Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. - Без Тора или ВПН, Ваш IP адрес будет доступен этим серверам файлов: %@. + Без Тора или ВПН, Ваш IP-адрес будет доступен этим серверам файлов: %@. alert message @@ -9187,7 +10134,7 @@ To connect, please ask your contact to create another connection link and check Wrong key or unknown file chunk address - most likely file is deleted. - Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удален. + Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удалён. file error text @@ -9197,7 +10144,7 @@ To connect, please ask your contact to create another connection link and check XFTP server - XFTP сервер + XFTP-сервер No comment provided by engineer. @@ -9264,7 +10211,7 @@ Repeat join request? You are connected to the server used to receive messages from this connection. - Вы подключены к серверу, используемому для приема сообщений от этого соединения. + Вы подключены к серверу, используемому для приёма сообщений от этого соединения. subscription status explanation @@ -9274,7 +10221,7 @@ Repeat join request? You are not connected to the server used to receive messages from this connection (no subscription). - Вы не подключены к серверу, используемому для получения сообщений по этому соединению (нет подписки). + Вы не подключены к серверу, через который Вы получали сообщения от этого контакта (нет подписки). subscription status explanation @@ -9314,7 +10261,7 @@ Repeat join request? You can give another try. - Вы можете попробовать еще раз. + Вы можете попробовать ещё раз. No comment provided by engineer. @@ -9347,9 +10294,14 @@ Repeat join request? Вы можете установить просмотр уведомлений на экране блокировки в настройках. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + Вы можете поделиться ссылкой или QR-кодом - любой сможет вступить в канал. + 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 кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились. + Вы можете поделиться ссылкой или QR-кодом - любой сможет присоединиться к группе. Члены группы останутся, даже если вы позже удалите ссылку. No comment provided by engineer. @@ -9359,7 +10311,7 @@ Repeat join request? You can start chat via app Settings / Database or by restarting the app - Вы можете запустить чат через Настройки приложения или перезапустив приложение. + Вы можете запустить чат через Настройки приложения или перезапустив приложение No comment provided by engineer. @@ -9392,14 +10344,23 @@ Repeat join request? Вы не можете отправлять сообщения! alert title - - You could not be verified; please try again. - Верификация не удалась; пожалуйста, попробуйте ещё раз. + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + Вы обязуетесь: +- Только законный контент в публичных группах +- Уважать других пользователей - без спама No comment provided by engineer. - - You decide who can connect. - Вы определяете, кто может соединиться. + + You connected to the channel via this relay link. + Вы подключились к каналу через эту ссылку релея. + No comment provided by engineer. + + + You could not be verified; please try again. + Ошибка аутентификации; попробуйте ещё раз. No comment provided by engineer. @@ -9411,7 +10372,7 @@ Repeat connection request? You have to enter passphrase every time the app starts - it is not stored on the device. - Пароль не сохранен на устройстве — Вы будете должны ввести его при каждом запуске чата. + Пароль не сохранён на устройстве - Вы будете должны ввести его при каждом запуске чата. No comment provided by engineer. @@ -9426,7 +10387,7 @@ Repeat connection request? You joined this group. Connecting to inviting group member. - Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы. + Вы вступили в группу. Устанавливается соединение с пригласившим Вас членом группы. No comment provided by engineer. @@ -9441,7 +10402,7 @@ Repeat connection request? 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. @@ -9469,6 +10430,11 @@ Repeat connection request? Вы должны получать уведомления. token info + + You were born without an account + Вы родились без аккаунта + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. Вы сможете отправлять сообщения **только после того как Ваш запрос будет принят**. @@ -9501,7 +10467,12 @@ Repeat connection request? 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 channel. Chat history will be preserved. + Вы перестанете получать сообщения из этого канала. История чата сохранится. No comment provided by engineer. @@ -9516,22 +10487,22 @@ Repeat connection request? You won't lose your contacts if you later delete your address. - Вы сможете удалить адрес, сохранив контакты, которые через него соединились. + Вы не потеряете контакты, если позже удалите Ваш адрес. 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 ICE servers - Ваши ICE серверы + Ваши ICE-серверы No comment provided by engineer. @@ -9541,7 +10512,7 @@ Repeat connection request? Your business contact - Ваш бизнес контакт + Ваш бизнес-контакт No comment provided by engineer. @@ -9549,6 +10520,11 @@ Repeat connection request? Ваши звонки No comment provided by engineer. + + Your channel + Ваш канал + No comment provided by engineer. + Your chat database База данных @@ -9599,9 +10575,14 @@ Repeat connection request? Ваши контакты сохранятся. No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + Ваши разговоры принадлежат вам, как это всегда было до интернета. Сеть - это не место, куда вы приходите. Это место, которое вы создаёте и которым владеете. И никто не может это у вас отнять, делаете ли вы его конфиденциальным или публичным. + No comment provided by engineer. + Your credentials may be sent unencrypted. - Ваши учетные данные могут быть отправлены в незашифрованном виде. + Ваши учётные данные могут быть отправлены в незашифрованном виде. No comment provided by engineer. @@ -9619,6 +10600,11 @@ Repeat connection request? Ваша группа No comment provided by engineer. + + Your network + Ваша сеть + No comment provided by engineer. + Your preferences Ваши предпочтения @@ -9634,6 +10620,13 @@ Repeat connection request? Ваш профиль No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + Ваш профиль **%@** будет отправлен чат-релеям и подписчикам канала. +Релеи могут видеть сообщения канала. + No comment provided by engineer. + Your profile **%@** will be shared. Будет отправлен Ваш профиль **%@**. @@ -9646,19 +10639,34 @@ Repeat connection request? Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. - Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. SimpleX серверы не могут получить доступ к Вашему профилю. + Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. Серверы SimpleX не могут получить доступ к Вашему профилю. No comment provided by engineer. Your profile was changed. If you save it, the updated profile will be sent to all your contacts. - Ваш профиль был изменен. Если вы сохраните его, обновленный профиль будет отправлен всем вашим контактам. + Ваш профиль был изменен. Если вы сохраните его, обновлённый профиль будет отправлен всем вашим контактам. alert message + + Your public address + Ваш публичный адрес + No comment provided by engineer. + Your random profile Случайный профиль No comment provided by engineer. + + Your relay address + Ваш адрес релея + No comment provided by engineer. + + + Your relay name + Ваше имя релея + No comment provided by engineer. + Your server address Адрес Вашего сервера @@ -9674,21 +10682,11 @@ Repeat connection request? Настройки 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) [Отправить email](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. - \_italic_ \_курсив_ @@ -9704,6 +10702,11 @@ Repeat connection request? наверху, затем выберите: No comment provided by engineer. + + accepted + принят(а) + No comment provided by engineer. + accepted %@ принят %@ @@ -9724,6 +10727,11 @@ Repeat connection request? Вы приняты rcv group event chat item + + active + активный + No comment provided by engineer. + admin админ @@ -9791,7 +10799,7 @@ Repeat connection request? bad message hash - ошибка хэш сообщения + ошибка хэша сообщения integrity error chat item @@ -9835,6 +10843,11 @@ marked deleted chat item preview text входящий звонок… call status + + can't broadcast + нельзя публиковать + No comment provided by engineer. + can't send messages нельзя отправлять @@ -9870,6 +10883,16 @@ marked deleted chat item preview text смена адреса… chat item text + + channel + канал + shown as sender role for channel messages + + + channel profile updated + профиль канала обновлён + snd group event chat item + colored цвет @@ -9887,7 +10910,7 @@ marked deleted chat item preview text connected - соединение установлено + соединен(а) No comment provided by engineer. @@ -9942,7 +10965,7 @@ marked deleted chat item preview text contact deleted - контакт удален + контакт удалён No comment provided by engineer. @@ -10016,6 +11039,11 @@ pref value удалено deleted chat item + + deleted channel + удалил(а) канал + rcv group event chat item + deleted contact удалил(а) контакт @@ -10038,7 +11066,7 @@ pref value disabled - выключено + выключен No comment provided by engineer. @@ -10126,6 +11154,11 @@ pref value ошибка No comment provided by engineer. + + error: %@ + ошибка: %@ + receive error chat item + expired истекло @@ -10133,6 +11166,7 @@ pref value failed + ошибка No comment provided by engineer. @@ -10157,7 +11191,7 @@ pref value group profile updated - профиль группы обновлен + профиль группы обновлён snd group event chat item @@ -10172,7 +11206,7 @@ pref value 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 после запуска чата или изменения пароля - это позволит получать мгновенные уведомления. + Пароль базы данных будет безопасно сохранён в iOS Keychain после запуска чата или изменения пароля - это позволит получать мгновенные уведомления. No comment provided by engineer. @@ -10255,6 +11289,11 @@ pref value покинул(а) группу rcv group event chat item + + link + ссылка + No comment provided by engineer. + marked deleted помечено к удалению @@ -10277,7 +11316,7 @@ pref value member has old version - член имеет старую версию + член группы имеет старую версию No comment provided by engineer. @@ -10325,6 +11364,11 @@ pref value никогда delete after time + + new + новый + No comment provided by engineer. + new message новое сообщение @@ -10448,6 +11492,11 @@ time to disappear отклонённый звонок call status + + relay + релей + member role + removed удален(а) @@ -10458,6 +11507,16 @@ time to disappear удалил(а) %@ rcv group event chat item + + removed (%d attempts) + удалено (%d попыток) + receive error chat item + + + removed by operator + удалено оператором + No comment provided by engineer. + removed contact address удалён адрес контакта @@ -10465,7 +11524,7 @@ time to disappear removed from group - удален из группы + удалён из группы No comment provided by engineer. @@ -10569,7 +11628,7 @@ last received msg: %2$@ standard end-to-end encryption - стандартное end-to-end шифрование + стандартное сквозное шифрование chat item text @@ -10612,6 +11671,11 @@ last received msg: %2$@ незащищённый No comment provided by engineer. + + updated channel profile + обновлён профиль канала + rcv group event chat item + updated group profile обновил(а) профиль группы @@ -10632,6 +11696,11 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + через %@ + relay hostname + via contact address link через ссылку-контакт @@ -10649,7 +11718,7 @@ last received msg: %2$@ via relay - через relay сервер + через релей-сервер No comment provided by engineer. @@ -10699,7 +11768,7 @@ last received msg: %2$@ you accepted this member - Вы приняли этого члена + Вы приняли этого члена группы snd group event chat item @@ -10707,6 +11776,11 @@ last received msg: %2$@ только чтение сообщений No comment provided by engineer. + + you are subscriber + Вы подписчик + No comment provided by engineer. + you blocked %@ Вы заблокировали %@ @@ -10767,6 +11841,11 @@ last received msg: %2$@ \~зачеркнуть~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + ⚠️ Ошибка проверки подписи: %@. + owner verification + @@ -10939,7 +12018,7 @@ last received msg: %2$@ Database passphrase is different from saved in the keychain. - Пароль базы данных отличается от сохраненного в keychain. + Пароль базы данных отличается от сохранённого в keychain. No comment provided by engineer. @@ -11019,7 +12098,7 @@ last received msg: %2$@ Please create a profile in the SimpleX app - Пожалуйста, создайте профиль в приложении SimpleX. + Пожалуйста, создайте профиль в приложении SimpleX No comment provided by engineer. 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 13d3240daf..04c51bbf43 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -167,6 +167,21 @@ %d เดือน time interval + + %d relays failed + channel relay bar +channel subscriber relay bar + + + %d relays not active + channel relay bar +channel subscriber relay bar + + + %d relays removed + channel relay bar +channel subscriber relay bar + %d sec %d วินาที @@ -181,11 +196,53 @@ %d ข้อความที่ถูกข้าม integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d สัปดาห์ time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + channel relay bar + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + channel subscriber relay bar + %lld %lld @@ -196,6 +253,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected % ผู้ติดต่อ LLD ที่เลือกไว้ @@ -290,10 +351,18 @@ %u ข้อความที่ถูกข้าม No comment provided by engineer. + + (from owner) + chat link info line + (new) No comment provided by engineer. + + (signed) + chat link info line + (this device v%@) No comment provided by engineer. @@ -334,6 +403,10 @@ **Scan / Paste link**: to connect via a link you received. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **คำเตือน**: การแจ้งเตือนแบบพุชทันทีจำเป็นต้องบันทึกรหัสผ่านไว้ใน Keychain @@ -373,6 +446,12 @@ - และอื่น ๆ! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -464,6 +543,10 @@ time interval อีกสองสามอย่าง No comment provided by engineer. + + A link for one person to connect + No comment provided by engineer. + A new contact ผู้ติดต่อใหม่ @@ -576,9 +659,8 @@ swipe action Active connections 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. - เพิ่มที่อยู่ลงในโปรไฟล์ของคุณ เพื่อให้ผู้ติดต่อของคุณสามารถแชร์กับผู้อื่นได้ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. @@ -638,6 +720,10 @@ swipe action Added message servers No comment provided by engineer. + + Adding relays will be supported later. + No comment provided by engineer. + Additional accent No comment provided by engineer. @@ -743,6 +829,14 @@ swipe action All profiles profile dropdown + + All relays failed + No comment provided by engineer. + + + All relays removed + No comment provided by engineer. + All reports will be archived for you. No comment provided by engineer. @@ -797,6 +891,10 @@ swipe action อนุญาตให้ลบข้อความแบบถาวรเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตให้คุณเท่านั้น No comment provided by engineer. + + Allow members to chat with admins. + No comment provided by engineer. + Allow message reactions only if your contact allows them. อนุญาตการแสดงปฏิกิริยาต่อข้อความเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น @@ -812,6 +910,10 @@ swipe action อนุญาตการส่งข้อความโดยตรงไปยังสมาชิก No comment provided by engineer. + + Allow sending direct messages to subscribers. + No comment provided by engineer. + Allow sending disappearing messages. อนุญาตให้ส่งข้อความที่จะหายไปหลังปิดแชท (disappearing message) @@ -821,6 +923,10 @@ swipe action Allow sharing No comment provided by engineer. + + Allow subscribers to chat with admins. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) อนุญาตให้ลบข้อความที่ส่งไปแล้วอย่างถาวร @@ -919,11 +1025,6 @@ swipe action รับสาย No comment provided by engineer. - - Anybody can host servers. - โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้ - No comment provided by engineer. - App build: %@ รุ่นแอป: %@ @@ -1110,6 +1211,19 @@ swipe action แฮชข้อความไม่ดี No comment provided by engineer. + + Be free +in your network + No comment provided by engineer. + + + Be free in your network. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + No comment provided by engineer. + Better calls No comment provided by engineer. @@ -1187,6 +1301,10 @@ swipe action Block member? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin No comment provided by engineer. @@ -1232,13 +1350,21 @@ swipe action ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความเสียงได้ No comment provided by engineer. + + Bottom bar + No comment provided by engineer. + + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. Business address - No comment provided by engineer. + chat link info line Business chats @@ -1257,12 +1383,6 @@ swipe action ตามโปรไฟล์แชท (ค่าเริ่มต้น) หรือ [โดยการเชื่อมต่อ](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (เบต้า) No comment provided by engineer. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - No comment provided by engineer. - Call already ended! สิ้นสุดการโทรแล้ว! @@ -1399,6 +1519,67 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + alert message +alert subtitle + + + Channel image + No comment provided by engineer. + + + Channel link + chat link info line + + + Channel preferences + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel temporarily unavailable + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + + + Channels + No comment provided by engineer. + Chat No comment provided by engineer. @@ -1475,6 +1656,22 @@ set passcode view โปรไฟล์ผู้ใช้ No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme No comment provided by engineer. @@ -1489,7 +1686,8 @@ set passcode view Chat with admins - chat toolbar + chat feature +chat toolbar Chat with member @@ -1504,10 +1702,22 @@ set passcode view แชท No comment provided by engineer. + + Chats with admins are prohibited. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + alert message + Chats with members No comment provided by engineer. + + Chats with members are disabled + No comment provided by engineer. + Check messages every 20 min. No comment provided by engineer. @@ -1516,6 +1726,14 @@ set passcode view Check messages when allowed. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. ตรวจสอบที่อยู่เซิร์ฟเวอร์แล้วลองอีกครั้ง @@ -1643,8 +1861,8 @@ set passcode view กำหนดค่าเซิร์ฟเวอร์ ICE No comment provided by engineer. - - Configure server operators + + Configure relays No comment provided by engineer. @@ -1699,7 +1917,8 @@ set passcode view Connect เชื่อมต่อ - server test step + relay test step +server test step Connect automatically @@ -1736,6 +1955,10 @@ This is your own one-time link! เชื่อมต่อผ่านลิงก์ new chat sheet title + + Connect via link or QR code + No comment provided by engineer. + Connect via one-time link new chat sheet title @@ -1803,7 +2026,7 @@ This is your own one-time link! Connection error (AUTH) การเชื่อมต่อผิดพลาด (AUTH) - No comment provided by engineer. + conn error description Connection failed @@ -1852,6 +2075,10 @@ This is your own one-time link! Connections No comment provided by engineer. + + Contact address + chat link info line + Contact allows ผู้ติดต่ออนุญาต @@ -1917,6 +2144,11 @@ This is your own one-time link! ดำเนินการต่อ No comment provided by engineer. + + Contribute + มีส่วนร่วม + No comment provided by engineer. + Conversation deleted! No comment provided by engineer. @@ -1941,12 +2173,7 @@ This is your own one-time link! Correct name to %@? - No comment provided by engineer. - - - Create - สร้าง - No comment provided by engineer. + alert message Create 1-time link @@ -1992,6 +2219,14 @@ This is your own one-time link! Create profile No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue สร้างคิว @@ -2001,11 +2236,19 @@ This is your own one-time link! Create your address No comment provided by engineer. + + Create your link + No comment provided by engineer. + Create your profile สร้างโปรไฟล์ของคุณ No comment provided by engineer. + + Create your public address + No comment provided by engineer. + Created No comment provided by engineer. @@ -2022,6 +2265,10 @@ This is your own one-time link! Creating archive link No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… No comment provided by engineer. @@ -2173,10 +2420,9 @@ This is your own one-time link! Debug delivery No comment provided by engineer. - - Decentralized - กระจายอำนาจแล้ว - No comment provided by engineer. + + Decode link + relay test step Decryption error @@ -2221,6 +2467,14 @@ swipe action Delete and notify contact No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat No comment provided by engineer. @@ -2382,6 +2636,10 @@ alert button ลบคิว server test step + + Delete relay + No comment provided by engineer. + Delete report No comment provided by engineer. @@ -2528,6 +2786,14 @@ alert button ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้ No comment provided by engineer. + + Direct messages between subscribers are prohibited. + No comment provided by engineer. + + + Disable + alert button + Disable (keep overrides) ปิดใช้งาน (เก็บการแทนที่) @@ -2624,6 +2890,10 @@ alert button Do not send history to new members. No comment provided by engineer. + + Do not send history to new subscribers. + No comment provided by engineer. + Do not use credentials with proxy. No comment provided by engineer. @@ -2712,11 +2982,19 @@ chat item action E2E encrypted notifications. No comment provided by engineer. + + Easier to invite your friends 👋 + No comment provided by engineer. + Edit แก้ไข chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile แก้ไขโปรไฟล์กลุ่ม @@ -2729,7 +3007,7 @@ chat item action Enable เปิดใช้งาน - No comment provided by engineer. + alert button Enable (keep overrides) @@ -2750,6 +3028,10 @@ chat item action เปิดใช้งาน TCP Keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? เปิดใช้งานการลบข้อความอัตโนมัติ? @@ -2759,6 +3041,10 @@ chat item action Enable camera access No comment provided by engineer. + + Enable chats with admins? + alert title + Enable disappearing messages by default. No comment provided by engineer. @@ -2777,16 +3063,15 @@ chat item action เปิดใช้งานการแจ้งเตือนทันที? No comment provided by engineer. + + Enable link previews? + alert title + Enable lock เปิดใช้งานการล็อค No comment provided by engineer. - - Enable notifications - เปิดใช้งานการแจ้งเตือน - No comment provided by engineer. - Enable periodic notifications? เปิดใช้การแจ้งเตือนเป็นระยะๆ ไหม? @@ -2884,6 +3169,10 @@ chat item action ใส่รหัสผ่าน No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. ใส่รหัสผ่านที่ถูกต้อง @@ -2907,6 +3196,14 @@ chat item action ใส่รหัสผ่านด้านบนเพื่อแสดง! No comment provided by engineer. + + Enter profile name... + No comment provided by engineer. + + + Enter relay name… + No comment provided by engineer. + Enter server manually ใส่เซิร์ฟเวอร์ด้วยตนเอง @@ -2933,7 +3230,7 @@ chat item action Error ผิดพลาด - No comment provided by engineer. + conn error description Error aborting address change @@ -2958,6 +3255,10 @@ chat item action เกิดข้อผิดพลาดในการเพิ่มสมาชิก No comment provided by engineer. + + Error adding relay + alert title + Error adding server alert title @@ -3010,6 +3311,10 @@ chat item action เกิดข้อผิดพลาดในการสร้างที่อยู่ No comment provided by engineer. + + Error creating channel + alert title + Error creating group เกิดข้อผิดพลาดในการสร้างกลุ่ม @@ -3134,10 +3439,6 @@ chat item action Error opening chat No comment provided by engineer. - - Error opening group - No comment provided by engineer. - Error receiving file เกิดข้อผิดพลาดในการรับไฟล์ @@ -3177,6 +3478,10 @@ chat item action เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ ICE No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list alert title @@ -3236,6 +3541,10 @@ chat item action เกิดข้อผิดพลาดในการตั้งค่าใบตอบรับการจัดส่ง! No comment provided by engineer. + + Error sharing channel + alert title + Error starting chat เกิดข้อผิดพลาดในการเริ่มแชท @@ -3310,7 +3619,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3527,7 +3837,8 @@ snd error text Fingerprint in server address does not match certificate. อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3567,9 +3878,14 @@ snd error text For all moderators No comment provided by engineer. + + For anyone to reach you + No comment provided by engineer. + For chat profile %@: - servers error + servers error +servers warning For console @@ -3688,10 +4004,18 @@ Error: %2$@ GIFs และสติกเกอร์ No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. No comment provided by engineer. + + Get started + No comment provided by engineer. + Good afternoon! message preview @@ -3746,7 +4070,7 @@ Error: %2$@ Group link ลิงค์กลุ่ม - No comment provided by engineer. + chat link info line Group links @@ -3854,6 +4178,10 @@ Error: %2$@ History is not sent to new members. No comment provided by engineer. + + History is not sent to new subscribers. + No comment provided by engineer. + How SimpleX works วิธีการ SimpleX ทํางานอย่างไร @@ -3947,11 +4275,6 @@ Error: %2$@ โดยทันที No comment provided by engineer. - - Immune to spam - มีภูมิคุ้มกันต่อสแปมและการละเมิด - No comment provided by engineer. - Import นำเข้า @@ -4081,9 +4404,9 @@ More improvements are coming soon! บทบาทเริ่มต้น 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) + + Install SimpleX Chat for terminal + ติดตั้ง SimpleX Chat สำหรับเทอร์มินัล No comment provided by engineer. @@ -4134,7 +4457,7 @@ More improvements are coming soon! Invalid connection link ลิงค์เชื่อมต่อไม่ถูกต้อง - No comment provided by engineer. + conn error description Invalid display name! @@ -4150,7 +4473,15 @@ More improvements are coming soon! Invalid name! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4184,6 +4515,10 @@ More improvements are coming soon! เชิญสมาชิก No comment provided by engineer. + + Invite someone privately + No comment provided by engineer. + Invite to chat No comment provided by engineer. @@ -4258,6 +4593,10 @@ More improvements are coming soon! เข้าร่วมเป็น %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group เข้าร่วมกลุ่ม @@ -4337,6 +4676,14 @@ This is your link for group %@! ออกจาก swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat No comment provided by engineer. @@ -4359,6 +4706,10 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let someone connect to you + No comment provided by engineer. + Let's talk in SimpleX Chat มาคุยกันใน SimpleX Chat @@ -4378,6 +4729,10 @@ This is your link for group %@! Link mobile and desktop apps! 🔗 No comment provided by engineer. + + Link signature verified. + owner verification + Linked desktop options No comment provided by engineer. @@ -4545,6 +4900,10 @@ This is your link for group %@! สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้ No comment provided by engineer. + + Members can chat with admins. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร @@ -4605,6 +4964,10 @@ This is your link for group %@! ร่างข้อความ No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded item status text @@ -4687,6 +5050,14 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + E2EE info chat item + Messages in this chat will never be deleted. alert message @@ -4711,12 +5082,12 @@ This is your link for group %@! Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery. No comment provided by engineer. - - Migrate device + + Migrate No comment provided by engineer. - - Migrate from another device + + Migrate device No comment provided by engineer. @@ -4829,6 +5200,10 @@ This is your link for group %@! เครือข่ายและเซิร์ฟเวอร์ No comment provided by engineer. + + Network commitments + No comment provided by engineer. + Network connection No comment provided by engineer. @@ -4837,6 +5212,10 @@ This is your link for group %@! Network decentralization No comment provided by engineer. + + Network error + conn error description + Network issues - message expired after many attempts to send it. snd error text @@ -4849,6 +5228,11 @@ This is your link for group %@! Network operator No comment provided by engineer. + + Network routers cannot know +who talks to whom + No comment provided by engineer. + Network settings การตั้งค่าเครือข่าย @@ -4863,6 +5247,10 @@ This is your link for group %@! New token status text + + New 1-time link + No comment provided by engineer. + New Passcode รหัสผ่านใหม่ @@ -4884,6 +5272,10 @@ This is your link for group %@! New chat experience 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request คำขอติดต่อใหม่ @@ -4948,11 +5340,28 @@ This is your link for group %@! เลขที่ No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + No comment provided by engineer. + + + No active relays + No comment provided by engineer. + No app password ไม่มีรหัสผ่านสำหรับแอป Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats No comment provided by engineer. @@ -5078,11 +5487,22 @@ This is your link for group %@! No unread chats No comment provided by engineer. - - No user identifiers. - แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. + + Non-profit governance + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + No comment provided by engineer. + + + Not all relays connected + alert title + Not compatible! No comment provided by engineer. @@ -5132,7 +5552,7 @@ This is your link for group %@! OK - No comment provided by engineer. + alert button Off @@ -5151,11 +5571,19 @@ new chat action ฐานข้อมูลเก่า No comment provided by engineer. + + On your phone, not on servers. + No comment provided by engineer. + One-time invitation link ลิงก์คำเชิญแบบใช้ครั้งเดียว No comment provided by engineer. + + One-time link + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5173,6 +5601,10 @@ Requires compatible VPN. โฮสต์หัวหอมจะไม่ถูกใช้ No comment provided by engineer. + + Only channel owners can change channel preferences. + No comment provided by engineer. + Only chat owners can change preferences. No comment provided by engineer. @@ -5269,7 +5701,8 @@ Requires compatible VPN. Open - alert action + alert action +alert button Open Settings @@ -5280,6 +5713,10 @@ Requires compatible VPN. Open changes No comment provided by engineer. + + Open channel + new chat action + Open chat เปิดแชท @@ -5298,6 +5735,10 @@ Requires compatible VPN. Open conditions No comment provided by engineer. + + Open external link? + alert title + Open full link alert action @@ -5314,6 +5755,10 @@ Requires compatible VPN. Open migration to another device authentication reason + + Open new channel + new chat action + Open new chat new chat action @@ -5350,6 +5795,13 @@ Requires compatible VPN. Operator server alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + No comment provided by engineer. + Or import archive file No comment provided by engineer. @@ -5366,6 +5818,10 @@ Requires compatible VPN. Or securely share this file link No comment provided by engineer. + + Or show QR in person or via video call. + No comment provided by engineer. + Or show this code No comment provided by engineer. @@ -5374,6 +5830,10 @@ Requires compatible VPN. Or to share privately No comment provided by engineer. + + Or use this QR - print or show online. + No comment provided by engineer. + Organize chats into lists No comment provided by engineer. @@ -5387,6 +5847,18 @@ Requires compatible VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + + + Ownership: you can run your own relays. + No comment provided by engineer. + PING count จํานวน PING @@ -5440,6 +5912,10 @@ Requires compatible VPN. แปะภาพ No comment provided by engineer. + + Paste link / Scan + No comment provided by engineer. + Paste link to connect! No comment provided by engineer. @@ -5578,6 +6054,14 @@ Error: %@ เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address ที่อยู่เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า @@ -5609,13 +6093,12 @@ Error: %@ Privacy policy and conditions of use. No comment provided by engineer. - - Privacy redefined - นิยามความเป็นส่วนตัวใหม่ + + Privacy: for owners and subscribers. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. + + Private and secure messaging. No comment provided by engineer. @@ -5651,6 +6134,10 @@ Error: %@ Private routing timeout alert title + + Proceed + alert action + Profile and server connections การเชื่อมต่อโปรไฟล์และเซิร์ฟเวอร์ @@ -5674,9 +6161,8 @@ Error: %@ Profile theme No comment provided by engineer. - - Profile update will be sent to your contacts. - การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ + + Profile update will be sent to your SimpleX contacts. alert message @@ -5684,6 +6170,10 @@ Error: %@ ห้ามการโทรด้วยเสียง/วิดีโอ No comment provided by engineer. + + Prohibit chats with admins. + No comment provided by engineer. + Prohibit irreversible message deletion. ห้ามการลบข้อความที่ย้อนกลับไม่ได้ @@ -5712,6 +6202,10 @@ Error: %@ ห้ามส่งข้อความโดยตรงถึงสมาชิก No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + No comment provided by engineer. + Prohibit sending disappearing messages. ห้ามส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) @@ -5772,6 +6266,10 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. + + Public channels - speak freely 🚀 + No comment provided by engineer. + Push notifications การแจ้งเตือนแบบทันที @@ -5809,23 +6307,14 @@ Enable in *Network & servers* settings. อ่านเพิ่มเติม No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + อ่านเพิ่มเติมในคู่มือผู้ใช้ No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) - 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) + + Read more in our GitHub repository. + อ่านเพิ่มเติมในพื้นที่เก็บข้อมูล GitHub No comment provided by engineer. @@ -5968,6 +6457,26 @@ swipe action Reject member? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + + + Relay results: + alert message + Relay server is only used if necessary. Another party can observe your IP address. ใช้เซิร์ฟเวอร์รีเลย์ในกรณีที่จำเป็นเท่านั้น บุคคลอื่นสามารถสังเกตที่อยู่ IP ของคุณได้ @@ -5978,6 +6487,14 @@ swipe action เซิร์ฟเวอร์รีเลย์ปกป้องที่อยู่ IP ของคุณ แต่สามารถสังเกตระยะเวลาของการโทรได้ No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + + + Reliability: many relays per channel. + No comment provided by engineer. + Remove ลบ @@ -6014,6 +6531,14 @@ swipe action ลบรหัสผ่านออกจาก keychain หรือไม่? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. No comment provided by engineer. @@ -6222,6 +6747,10 @@ swipe action SOCKS proxy No comment provided by engineer. + + Safe web links + No comment provided by engineer. + Safely receive files No comment provided by engineer. @@ -6245,6 +6774,10 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + alert button + Save admission settings? alert title @@ -6259,6 +6792,10 @@ chat item action บันทึกและแจ้งให้สมาชิกในกลุ่มทราบ No comment provided by engineer. + + Save and notify subscribers + No comment provided by engineer. + Save and reconnect No comment provided by engineer. @@ -6268,6 +6805,14 @@ chat item action บันทึกและอัปเดตโปรไฟล์กลุ่ม No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile บันทึกโปรไฟล์กลุ่ม @@ -6429,6 +6974,10 @@ chat item action รหัสความปลอดภัย No comment provided by engineer. + + Security: owners hold channel keys. + No comment provided by engineer. + Select เลือก @@ -6547,6 +7096,10 @@ chat item action Send request without message No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. ส่งจากแกลเลอรีหรือแป้นพิมพ์แบบกำหนดเอง @@ -6556,6 +7109,10 @@ chat item action Send up to 100 last messages to new members. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + No comment provided by engineer. + Send your private feedback to groups. No comment provided by engineer. @@ -6570,6 +7127,10 @@ chat item action ผู้ส่งอาจลบคําขอการเชื่อมต่อแล้ว No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. การส่งใบเสร็จรับการจัดส่งข้อความจะถูกเปิดในโปรไฟล์แชทที่มองเห็นได้ทั้งหมด @@ -6680,6 +7241,10 @@ chat item action Server protocol changed. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน @@ -6797,6 +7362,14 @@ chat item action Settings were changed. alert message + + Setup notifications + No comment provided by engineer. + + + Setup routers + No comment provided by engineer. + Shape profile images No comment provided by engineer. @@ -6829,11 +7402,14 @@ chat item action Share address publicly No comment provided by engineer. - - Share address with contacts? - แชร์ที่อยู่กับผู้ติดต่อ? + + Share address with SimpleX contacts? alert title + + Share channel + No comment provided by engineer. + Share from other apps. No comment provided by engineer. @@ -6855,6 +7431,10 @@ chat item action Share profile No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. @@ -6863,9 +7443,12 @@ chat item action Share to SimpleX No comment provided by engineer. - - Share with contacts - แชร์กับผู้ติดต่อ + + Share via chat + No comment provided by engineer. + + + Share with SimpleX contacts No comment provided by engineer. @@ -7020,8 +7603,8 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7086,6 +7669,11 @@ report reason Square, circle, or anything in between. No comment provided by engineer. + + Star on GitHub + ติดดาวบน GitHub + No comment provided by engineer. + Start chat เริ่มแชท @@ -7178,6 +7766,63 @@ report reason Subscribed No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber reports + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers can add message reactions. + No comment provided by engineer. + + + Subscribers can chat with admins. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + No comment provided by engineer. + + + Subscribers can send direct messages. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + No comment provided by engineer. + + + Subscribers can send files and media. + No comment provided by engineer. + + + Subscribers can send voice messages. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors No comment provided by engineer. @@ -7250,6 +7895,10 @@ report reason ถ่ายภาพ No comment provided by engineer. + + Talk to someone + No comment provided by engineer. + Tap Connect to chat No comment provided by engineer. @@ -7262,8 +7911,8 @@ report reason Tap Connect to use bot No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. + + Tap Join channel No comment provided by engineer. @@ -7294,6 +7943,10 @@ report reason แตะเพื่อเข้าร่วมโหมดไม่ระบุตัวตน No comment provided by engineer. + + Tap to open + No comment provided by engineer. + Tap to paste link No comment provided by engineer. @@ -7309,12 +7962,17 @@ report reason Test failed at step %@. การทดสอบล้มเหลวในขั้นตอน %@ - server test failure + relay test failure +server test failure Test notifications No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server เซิร์ฟเวอร์ทดสอบ @@ -7366,6 +8024,10 @@ It can happen because of some bug or when the connection is compromised.The app protects your privacy by using different operators in each conversation. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -7379,6 +8041,10 @@ It can happen because of some bug or when the connection is compromised.The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. No comment provided by engineer. @@ -7403,9 +8069,9 @@ It can happen because of some bug or when the connection is compromised.encryption กำลังทำงานและไม่จำเป็นต้องใช้ข้อตกลง encryption ใหม่ อาจทำให้การเชื่อมต่อผิดพลาดได้! No comment provided by engineer. - - The future of messaging - การส่งข้อความส่วนตัวรุ่นต่อไป + + The first network where you own +your contacts and groups. No comment provided by engineer. @@ -7440,6 +8106,10 @@ It can happen because of some bug or when the connection is compromised.ฐานข้อมูลเก่าไม่ได้ถูกลบในระหว่างการย้ายข้อมูล แต่สามารถลบได้ No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + No comment provided by engineer. + The same conditions will apply to operator **%@**. No comment provided by engineer. @@ -7479,6 +8149,14 @@ It can happen because of some bug or when the connection is compromised.Themes No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + No comment provided by engineer. + These conditions will also apply for: **%@**. No comment provided by engineer. @@ -7536,6 +8214,14 @@ It can happen because of some bug or when the connection is compromised.ไม่มีกลุ่มนี้แล้ว No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -7579,6 +8265,10 @@ It can happen because of some bug or when the connection is compromised.To hide unwanted messages. No comment provided by engineer. + + To make SimpleX Network last. + No comment provided by engineer. + To make a new connection เพื่อสร้างการเชื่อมต่อใหม่ @@ -7657,10 +8347,6 @@ You will be prompted to complete authentication before this feature is enabled.< ในการตรวจสอบการเข้ารหัสแบบ encrypt จากต้นจนจบ กับผู้ติดต่อของคุณ ให้เปรียบเทียบ (หรือสแกน) รหัสบนอุปกรณ์ของคุณ No comment provided by engineer. - - Toggle chat list: - No comment provided by engineer. - Toggle incognito when connecting. No comment provided by engineer. @@ -7673,6 +8359,10 @@ You will be prompted to complete authentication before this feature is enabled.< Toolbar opacity No comment provided by engineer. + + Top bar + No comment provided by engineer. + Total No comment provided by engineer. @@ -7729,6 +8419,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -7824,12 +8518,16 @@ To connect, please ask your contact to create another connection link and check Unsupported connection link - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + No comment provided by engineer. + Update อัปเดต @@ -7938,11 +8636,6 @@ To connect, please ask your contact to create another connection link and check Use TCP port 443 for preset servers only. No comment provided by engineer. - - Use chat - ใช้แชท - No comment provided by engineer. - Use current profile new chat action @@ -7955,6 +8648,10 @@ To connect, please ask your contact to create another connection link and check Use for messages No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections ใช้สำหรับการเชื่อมต่อใหม่ @@ -7989,6 +8686,10 @@ To connect, please ask your contact to create another connection link and check Use private routing with unknown servers. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server ใช้เซิร์ฟเวอร์ @@ -8006,6 +8707,10 @@ To connect, please ask your contact to create another connection link and check Use the app with one hand. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + No comment provided by engineer. + Use web port No comment provided by engineer. @@ -8023,6 +8728,10 @@ To connect, please ask your contact to create another connection link and check กำลังใช้เซิร์ฟเวอร์ SimpleX Chat อยู่ No comment provided by engineer. + + Verify + relay test step + Verify code with desktop No comment provided by engineer. @@ -8132,6 +8841,18 @@ To connect, please ask your contact to create another connection link and check ข้อความเสียง… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + + + Waiting for channel owner to add relays. + No comment provided by engineer. + Waiting for desktop... No comment provided by engineer. @@ -8168,6 +8889,10 @@ To connect, please ask your contact to create another connection link and check คำเตือน: คุณอาจสูญเสียข้อมูลบางส่วน! No comment provided by engineer. + + We made connecting simpler for new users. + No comment provided by engineer. + WebRTC ICE servers เซิร์ฟเวอร์ WebRTC ICE @@ -8214,6 +8939,10 @@ To connect, please ask your contact to create another connection link and check เมื่อคุณแชร์โปรไฟล์ที่ไม่ระบุตัวตนกับใครสักคน โปรไฟล์นี้จะใช้สำหรับกลุ่มที่พวกเขาเชิญคุณ No comment provided by engineer. + + Why SimpleX is built. + No comment provided by engineer. + WiFi No comment provided by engineer. @@ -8398,6 +9127,10 @@ Repeat join request? คุณสามารถตั้งค่าแสดงตัวอย่างการแจ้งเตือนบนหน้าจอล็อคผ่านการตั้งค่า No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + 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. คุณสามารถแชร์ลิงก์หรือคิวอาร์โค้ดได้ ทุกคนจะสามารถเข้าร่วมกลุ่มได้ คุณจะไม่สูญเสียสมาชิกของกลุ่มหากคุณลบในภายหลัง @@ -8440,16 +9173,21 @@ Repeat join request? คุณไม่สามารถส่งข้อความได้! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. เราไม่สามารถตรวจสอบคุณได้ กรุณาลองอีกครั้ง. No comment provided by engineer. - - You decide who can connect. - ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -8510,6 +9248,10 @@ Repeat connection request? You should receive notifications. token info + + You were born without an account + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. No comment provided by engineer. @@ -8543,6 +9285,10 @@ Repeat connection request? คุณจะยังได้รับสายเรียกเข้าและการแจ้งเตือนจากโปรไฟล์ที่ปิดเสียงเมื่อโปรไฟล์ของเขามีการใช้งาน No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. No comment provided by engineer. @@ -8586,6 +9332,10 @@ Repeat connection request? การโทรของคุณ No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database ฐานข้อมูลการแชทของคุณ @@ -8632,6 +9382,10 @@ Repeat connection request? ผู้ติดต่อของคุณจะยังคงเชื่อมต่ออยู่ No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + No comment provided by engineer. + Your credentials may be sent unencrypted. No comment provided by engineer. @@ -8650,6 +9404,10 @@ Repeat connection request? Your group No comment provided by engineer. + + Your network + No comment provided by engineer. + Your preferences การตั้งค่าของคุณ @@ -8664,6 +9422,11 @@ Repeat connection request? Your profile No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. No comment provided by engineer. @@ -8682,11 +9445,23 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. alert message + + Your public address + No comment provided by engineer. + Your random profile โปรไฟล์แบบสุ่มของคุณ No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address ที่อยู่เซิร์ฟเวอร์ของคุณ @@ -8701,21 +9476,11 @@ Repeat connection request? การตั้งค่าของคุณ 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. - \_italic_ \_ตัวเอียง_ @@ -8731,6 +9496,10 @@ Repeat connection request? ด้านบน จากนั้นเลือก: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ rcv group event chat item @@ -8748,6 +9517,10 @@ Repeat connection request? accepted you rcv group event chat item + + active + No comment provided by engineer. + admin ผู้ดูแลระบบ @@ -8848,6 +9621,10 @@ marked deleted chat item preview text กำลังโทร… call status + + can't broadcast + No comment provided by engineer. + can't send messages No comment provided by engineer. @@ -8882,6 +9659,14 @@ marked deleted chat item preview text กำลังเปลี่ยนที่อยู่… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored มีสี @@ -9022,6 +9807,10 @@ pref value ลบแล้ว deleted chat item + + deleted channel + rcv group event chat item + deleted contact rcv direct event chat item @@ -9129,6 +9918,10 @@ pref value ผิดพลาด No comment provided by engineer. + + error: %@ + receive error chat item + expired No comment provided by engineer. @@ -9252,6 +10045,10 @@ pref value ออกแล้ว rcv group event chat item + + link + No comment provided by engineer. + marked deleted ทำเครื่องหมายว่าลบแล้ว @@ -9318,6 +10115,10 @@ pref value ไม่เคย delete after time + + new + No comment provided by engineer. + new message ข้อความใหม่ @@ -9431,6 +10232,10 @@ time to disappear สายถูกปฏิเสธ call status + + relay + member role + removed ถูกลบแล้ว @@ -9441,6 +10246,14 @@ time to disappear ถูกลบแล้ว %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + + + removed by operator + No comment provided by engineer. + removed contact address profile update event chat item @@ -9572,6 +10385,10 @@ last received msg: %2$@ unprotected No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile อัปเดตโปรไฟล์กลุ่มแล้ว @@ -9590,6 +10407,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link ผ่านลิงค์ที่อยู่ติดต่อ @@ -9661,6 +10482,10 @@ last received msg: %2$@ คุณเป็นผู้สังเกตการณ์ No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ snd group event chat item @@ -9719,6 +10544,10 @@ last received msg: %2$@ \~ตี~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + owner verification + 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 c97da9e0b5..90d537d06c 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -185,6 +185,21 @@ %d ay time interval + + %d relays failed + channel relay bar +channel subscriber relay bar + + + %d relays not active + channel relay bar +channel subscriber relay bar + + + %d relays removed + channel relay bar +channel subscriber relay bar + %d sec %d saniye @@ -200,11 +215,53 @@ %d okunmamış mesaj(lar) integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d hafta time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + channel relay bar + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + channel subscriber relay bar + %lld %lld @@ -215,6 +272,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld kişi seçildi @@ -315,11 +376,19 @@ %u mesajlar atlandı. No comment provided by engineer. + + (from owner) + chat link info line + (new) (yeni) No comment provided by engineer. + + (signed) + chat link info line + (this device v%@) (bu cihaz v%@) @@ -365,6 +434,10 @@ edindiğiniz bağlantı aracılığıyla bağlanmak için **Linki tarayın/yapıştırın**. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Dikkat**: Anında iletilen bildirimlere Anahtar Zinciri'nde kaydedilmiş parola gereklidir. @@ -408,6 +481,12 @@ - ve fazlası! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -506,6 +585,10 @@ time interval Birkaç şey daha No comment provided by engineer. + + A link for one person to connect + No comment provided by engineer. + A new contact Yeni kişi @@ -632,9 +715,8 @@ swipe action Aktif bağlantılar 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. - Kişilerinizin başkalarıyla paylaşabilmesi için profilinize adres ekleyin. Profil güncellemesi kişilerinize gönderilecek. + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. @@ -702,6 +784,10 @@ swipe action Mesaj sunucuları eklendi No comment provided by engineer. + + Adding relays will be supported later. + No comment provided by engineer. + Additional accent Ek ana renk @@ -821,6 +907,14 @@ swipe action Tüm Profiller profile dropdown + + All relays failed + No comment provided by engineer. + + + All relays removed + No comment provided by engineer. + All reports will be archived for you. Tüm raporlar sizin için arşivlenecek. @@ -881,6 +975,10 @@ swipe action Konuştuğun kişi, kalıcı olarak silinebilen mesajlara izin veriyorsa sen de ver. (24 saat içinde) No comment provided by engineer. + + Allow members to chat with admins. + No comment provided by engineer. + Allow message reactions only if your contact allows them. Yalnızca kişin mesaj tepkilerine izin veriyorsa sen de ver. @@ -896,6 +994,10 @@ swipe action Üyelere doğrudan mesaj göndermeye izin ver. No comment provided by engineer. + + Allow sending direct messages to subscribers. + No comment provided by engineer. + Allow sending disappearing messages. Kendiliğinden yok olan mesajlar göndermeye izin ver. @@ -906,6 +1008,10 @@ swipe action Paylaşıma izin ver No comment provided by engineer. + + Allow subscribers to chat with admins. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Gönderilen mesajların kalıcı olarak silinmesine izin ver. (24 saat içinde) @@ -1011,11 +1117,6 @@ swipe action Aramayı cevapla No comment provided by engineer. - - Anybody can host servers. - Açık kaynak protokolü ve kodu - herhangi biri sunucuları çalıştırabilir. - No comment provided by engineer. - App build: %@ Uygulama sürümü: %@ @@ -1220,6 +1321,19 @@ swipe action Kötü mesaj karması No comment provided by engineer. + + Be free +in your network + No comment provided by engineer. + + + Be free in your network. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + No comment provided by engineer. + Better calls Daha iyi aramalar @@ -1315,6 +1429,10 @@ swipe action Üyeyi engelle? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Yönetici tarafından engellendi @@ -1365,6 +1483,14 @@ swipe action Sen ve konuştuğun kişi sesli mesaj gönderebilir. No comment provided by engineer. + + Bottom bar + No comment provided by engineer. + + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Bulgarca, Fince, Tayca ve Ukraynaca - kullanıcılara ve [Weblate] e teşekkürler! (https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1373,7 +1499,7 @@ swipe action Business address İş adresi - No comment provided by engineer. + chat link info line Business chats @@ -1395,15 +1521,6 @@ swipe action Sohbet profiline göre (varsayılan) veya [bağlantıya göre](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - SimpleX Chat'i kullanarak şunları kabul etmiş olursunuz: -- herkese açık gruplarda yalnızca yasal içerik göndermek. -- diğer kullanıcılara saygı göstermek – spam yapmamak. - No comment provided by engineer. - Call already ended! Arama çoktan bitti! @@ -1552,6 +1669,67 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + alert message +alert subtitle + + + Channel image + No comment provided by engineer. + + + Channel link + chat link info line + + + Channel preferences + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel temporarily unavailable + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + + + Channels + No comment provided by engineer. + Chat Sohbet @@ -1637,6 +1815,22 @@ set passcode view Kullanıcı profili No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Sohbet teması @@ -1655,7 +1849,8 @@ set passcode view Chat with admins Yöneticilerle sohbet et - chat toolbar + chat feature +chat toolbar Chat with member @@ -1672,11 +1867,23 @@ set passcode view Sohbetler No comment provided by engineer. + + Chats with admins are prohibited. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + alert message + Chats with members Üyelerle sohbetler No comment provided by engineer. + + Chats with members are disabled + No comment provided by engineer. + Check messages every 20 min. Her 20 dakikada mesajları kontrol et. @@ -1687,6 +1894,14 @@ set passcode view İzin verildiğinde mesajları kontrol et. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Sunucu adresini kontrol edip tekrar deneyin. @@ -1832,9 +2047,8 @@ set passcode view ICE sunucularını ayarla No comment provided by engineer. - - Configure server operators - Sunucu operatörlerini yapılandır + + Configure relays No comment provided by engineer. @@ -1895,7 +2109,8 @@ set passcode view Connect Bağlan - server test step + relay test step +server test step Connect automatically @@ -1941,6 +2156,10 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı aracılığıyla bağlan new chat sheet title + + Connect via link or QR code + No comment provided by engineer. + Connect via one-time link Tek kullanımlık bağlantı aracılığıyla bağlan @@ -2019,7 +2238,7 @@ Bu senin kendi tek kullanımlık bağlantın! Connection error (AUTH) Bağlantı hatası (DOĞRULAMA) - No comment provided by engineer. + conn error description Connection failed @@ -2077,6 +2296,10 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantılar No comment provided by engineer. + + Contact address + chat link info line + Contact allows Kişi izin veriyor @@ -2147,6 +2370,11 @@ Bu senin kendi tek kullanımlık bağlantın! Devam et No comment provided by engineer. + + Contribute + Katkıda bulun + No comment provided by engineer. + Conversation deleted! Sohbet silindi! @@ -2175,12 +2403,7 @@ Bu senin kendi tek kullanımlık bağlantın! Correct name to %@? İsim %@ olarak düzeltilsin mi? - No comment provided by engineer. - - - Create - Oluştur - No comment provided by engineer. + alert message Create 1-time link @@ -2232,6 +2455,14 @@ Bu senin kendi tek kullanımlık bağlantın! Profil oluştur No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Sıra oluştur @@ -2242,11 +2473,19 @@ Bu senin kendi tek kullanımlık bağlantın! Adresinizi oluşturun No comment provided by engineer. + + Create your link + No comment provided by engineer. + Create your profile Profilini oluştur No comment provided by engineer. + + Create your public address + No comment provided by engineer. + Created Yaratıldı @@ -2267,6 +2506,10 @@ Bu senin kendi tek kullanımlık bağlantın! Arşiv bağlantısı oluşturuluyor No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Link oluşturuluyor… @@ -2425,10 +2668,9 @@ Bu senin kendi tek kullanımlık bağlantın! Hata ayıklama teslimatı No comment provided by engineer. - - Decentralized - Merkezi Olmayan - No comment provided by engineer. + + Decode link + relay test step Decryption error @@ -2476,6 +2718,14 @@ swipe action Sil ve kişiye bildir No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Sohbeti sil @@ -2645,6 +2895,10 @@ alert button Sırayı sil server test step + + Delete relay + No comment provided by engineer. + Delete report Raporu sil @@ -2810,6 +3064,14 @@ alert button Bu grupta üyeler arasında direkt mesajlaşma yasaktır. No comment provided by engineer. + + Direct messages between subscribers are prohibited. + No comment provided by engineer. + + + Disable + alert button + Disable (keep overrides) Devre dışı bırak (geçersiz kılmaları koru) @@ -2915,6 +3177,10 @@ alert button Yeni üyelere geçmişi gönderme. No comment provided by engineer. + + Do not send history to new subscribers. + No comment provided by engineer. + Do not use credentials with proxy. Kimlik bilgilerini proxy ile kullanmayın. @@ -3016,11 +3282,19 @@ chat item action Uçtan uca şifrelenmiş bildirimler. No comment provided by engineer. + + Easier to invite your friends 👋 + No comment provided by engineer. + Edit Düzenle chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Grup profilini düzenle @@ -3034,7 +3308,7 @@ chat item action Enable Etkinleştir - No comment provided by engineer. + alert button Enable (keep overrides) @@ -3056,6 +3330,10 @@ chat item action TCP canlı tutmayı etkinleştir No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Otomatik mesaj silme etkinleştirilsin mi? @@ -3066,6 +3344,10 @@ chat item action Kamera erişimini etkinleştir No comment provided by engineer. + + Enable chats with admins? + alert title + Enable disappearing messages by default. Varsayılan olarak kaybolan mesajları etkinleştirin. @@ -3086,16 +3368,15 @@ chat item action Anlık bildirimler etkinleştirilsin mi? No comment provided by engineer. + + Enable link previews? + alert title + Enable lock Kilidi etkinleştir No comment provided by engineer. - - Enable notifications - Bildirimleri etkinleştir - No comment provided by engineer. - Enable periodic notifications? Periyodik bildirimler etkinleştirilsin mi? @@ -3201,6 +3482,10 @@ chat item action Şifre gir No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Doğru şifreyi gir. @@ -3226,6 +3511,14 @@ chat item action Göstermek için yukarıdaki şifreyi gir! No comment provided by engineer. + + Enter profile name... + No comment provided by engineer. + + + Enter relay name… + No comment provided by engineer. + Enter server manually Sunucuya manuel olarak gir @@ -3254,7 +3547,7 @@ chat item action Error Hata - No comment provided by engineer. + conn error description Error aborting address change @@ -3281,6 +3574,10 @@ chat item action Üye(ler) eklenirken hata oluştu No comment provided by engineer. + + Error adding relay + alert title + Error adding server Sunucu eklenirken hata oluştu @@ -3340,6 +3637,10 @@ chat item action Adres oluşturulurken hata oluştu No comment provided by engineer. + + Error creating channel + alert title + Error creating group Grup oluşturulurken hata oluştu @@ -3475,11 +3776,6 @@ chat item action Kişiyi hazırlama hatası No comment provided by engineer. - - Error opening group - Grubu hazırlama hatası - No comment provided by engineer. - Error receiving file Dosya alınırken sorun oluştu @@ -3525,6 +3821,10 @@ chat item action ICE sunucularını kaydedirken sorun oluştu No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Sohbet listesini kaydetme hatası @@ -3590,6 +3890,10 @@ chat item action Görüldü ayarlanırken hata oluştu! No comment provided by engineer. + + Error sharing channel + alert title + Error starting chat Sohbet başlatılırken hata oluştu @@ -3669,7 +3973,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3910,7 +4215,8 @@ snd error text Fingerprint in server address does not match certificate. Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3951,10 +4257,15 @@ snd error text Tüm moderatörler için No comment provided by engineer. + + For anyone to reach you + No comment provided by engineer. + For chat profile %@: Sohbet profili için %@: - servers error + servers error +servers warning For console @@ -4095,11 +4406,19 @@ Hata: %2$@ GİFler ve çıkartmalar No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. Bahsedildiğinde bildirim alın. No comment provided by engineer. + + Get started + No comment provided by engineer. + Good afternoon! İyi öğlenler! @@ -4158,7 +4477,7 @@ Hata: %2$@ Group link Grup bağlantısı - No comment provided by engineer. + chat link info line Group links @@ -4270,6 +4589,10 @@ Hata: %2$@ Yeni üyelere geçmiş gönderilmedi. No comment provided by engineer. + + History is not sent to new subscribers. + No comment provided by engineer. + How SimpleX works SimpleX nasıl çalışır @@ -4368,11 +4691,6 @@ Hata: %2$@ Hemen No comment provided by engineer. - - Immune to spam - Spam ve kötüye kullanıma karşı bağışıklı - No comment provided by engineer. - Import İçe aktar @@ -4515,9 +4833,9 @@ Daha fazla iyileştirme yakında geliyor! Başlangıç rolü No comment provided by engineer. - - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - [Terminal için SimpleX Chat]i indir(https://github.com/simplex-chat/simplex-chat) + + Install SimpleX Chat for terminal + Terminal için SimpleX Chat'i indir No comment provided by engineer. @@ -4575,7 +4893,7 @@ Daha fazla iyileştirme yakında geliyor! Invalid connection link Geçersiz bağlanma bağlantısı - No comment provided by engineer. + conn error description Invalid display name! @@ -4595,7 +4913,15 @@ Daha fazla iyileştirme yakında geliyor! Invalid name! Geçersiz isim! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4631,6 +4957,10 @@ Daha fazla iyileştirme yakında geliyor! Üyeleri davet et No comment provided by engineer. + + Invite someone privately + No comment provided by engineer. + Invite to chat Sohbete davet et @@ -4707,6 +5037,10 @@ Daha fazla iyileştirme yakında geliyor! %@ olarak katıl No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Gruba katıl @@ -4794,6 +5128,14 @@ Bu senin grup için bağlantın %@! Ayrıl swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Sohbetten ayrıl @@ -4819,6 +5161,10 @@ Bu senin grup için bağlantın %@! Mobil ağlarda daha az trafik. No comment provided by engineer. + + Let someone connect to you + No comment provided by engineer. + Let's talk in SimpleX Chat Hadi SimpleX Chat'te konuşalım @@ -4839,6 +5185,10 @@ Bu senin grup için bağlantın %@! Telefon ve bilgisayar uygulamalarını bağla! 🔗 No comment provided by engineer. + + Link signature verified. + owner verification + Linked desktop options Bağlanmış bilgisayar ayarları @@ -5022,6 +5372,10 @@ Bu senin grup için bağlantın %@! Grup üyeleri mesaj tepkileri ekleyebilir. No comment provided by engineer. + + Members can chat with admins. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) Grup üyeleri, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde) @@ -5087,6 +5441,10 @@ Bu senin grup için bağlantın %@! Mesaj taslağı No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Mesaj iletildi @@ -5182,6 +5540,14 @@ Bu senin grup için bağlantın %@! %@ den gelen mesajlar gösterilecektir! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + E2EE info chat item + Messages in this chat will never be deleted. Bu sohbetteki mesajlar asla silinmeyecek. @@ -5212,16 +5578,15 @@ Bu senin grup için bağlantın %@! Mesajlar, dosyalar ve aramalar **kuantum dirençli e2e şifreleme** ile mükemmel ileri gizlilik, inkar ve zorla girme kurtarma ile korunur. No comment provided by engineer. + + Migrate + No comment provided by engineer. + Migrate device Cihazı taşıma No comment provided by engineer. - - Migrate from another device - Başka bir cihazdan geçiş yapın - No comment provided by engineer. - Migrate here Buraya göç edin @@ -5342,6 +5707,10 @@ Bu senin grup için bağlantın %@! Ağ & sunucular No comment provided by engineer. + + Network commitments + No comment provided by engineer. + Network connection Ağ bağlantısı @@ -5352,6 +5721,10 @@ Bu senin grup için bağlantın %@! Ağ merkeziyetsizliği No comment provided by engineer. + + Network error + conn error description + Network issues - message expired after many attempts to send it. Ağ sorunları - birçok gönderme denemesinden sonra mesajın süresi doldu. @@ -5367,6 +5740,11 @@ Bu senin grup için bağlantın %@! Ağ operatörü No comment provided by engineer. + + Network routers cannot know +who talks to whom + No comment provided by engineer. + Network settings Ağ ayarları @@ -5382,6 +5760,10 @@ Bu senin grup için bağlantın %@! Yeni token status text + + New 1-time link + No comment provided by engineer. + New Passcode Yeni şifre @@ -5407,6 +5789,10 @@ Bu senin grup için bağlantın %@! Yeni bir sohbet deneyimi 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Yeni bağlantı isteği @@ -5477,11 +5863,28 @@ Bu senin grup için bağlantın %@! Hayır No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + No comment provided by engineer. + + + No active relays + No comment provided by engineer. + No app password Uygulama şifresi yok Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats Hiç sohbet yok @@ -5627,11 +6030,22 @@ Bu senin grup için bağlantın %@! Okunmamış sohbet yok No comment provided by engineer. - - No user identifiers. - Herhangi bir kullanıcı tanımlayıcısı yok. + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. + + Non-profit governance + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + No comment provided by engineer. + + + Not all relays connected + alert title + Not compatible! Uyumlu değil! @@ -5689,7 +6103,7 @@ Bu senin grup için bağlantın %@! OK TAMAM - No comment provided by engineer. + alert button Off @@ -5708,11 +6122,19 @@ new chat action Eski veritabanı No comment provided by engineer. + + On your phone, not on servers. + No comment provided by engineer. + One-time invitation link Tek zamanlı bağlantı daveti No comment provided by engineer. + + One-time link + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5732,6 +6154,10 @@ VPN'nin etkinleştirilmesi gerekir. Onion ana bilgisayarları kullanılmayacaktır. No comment provided by engineer. + + Only channel owners can change channel preferences. + No comment provided by engineer. + Only chat owners can change preferences. Yalnızca sohbet sahipleri tercihleri değiştirebilir. @@ -5835,7 +6261,8 @@ VPN'nin etkinleştirilmesi gerekir. Open - alert action + alert action +alert button Open Settings @@ -5847,6 +6274,10 @@ VPN'nin etkinleştirilmesi gerekir. Açık değişiklikler No comment provided by engineer. + + Open channel + new chat action + Open chat Sohbeti aç @@ -5867,6 +6298,10 @@ VPN'nin etkinleştirilmesi gerekir. Açık koşullar No comment provided by engineer. + + Open external link? + alert title + Open full link Tam linki aç @@ -5887,6 +6322,10 @@ VPN'nin etkinleştirilmesi gerekir. Başka bir cihaza açık geçiş authentication reason + + Open new channel + new chat action + Open new chat Yeni sohbet aç @@ -5932,6 +6371,13 @@ VPN'nin etkinleştirilmesi gerekir. Operatör sunucusu alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + No comment provided by engineer. + Or import archive file Veya arşiv dosyasını içe aktar @@ -5952,6 +6398,10 @@ VPN'nin etkinleştirilmesi gerekir. Veya bu dosya bağlantısını güvenli bir şekilde paylaşın No comment provided by engineer. + + Or show QR in person or via video call. + No comment provided by engineer. + Or show this code Veya bu kodu göster @@ -5962,6 +6412,10 @@ VPN'nin etkinleştirilmesi gerekir. Veya özel olarak paylaşmak için No comment provided by engineer. + + Or use this QR - print or show online. + No comment provided by engineer. + Organize chats into lists Sohbetleri listelere ayır @@ -5979,6 +6433,18 @@ VPN'nin etkinleştirilmesi gerekir. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + + + Ownership: you can run your own relays. + No comment provided by engineer. + PING count PING sayısı @@ -6034,6 +6500,10 @@ VPN'nin etkinleştirilmesi gerekir. Fotoğraf yapıştır No comment provided by engineer. + + Paste link / Scan + No comment provided by engineer. + Paste link to connect! Bağlanmak için bağlantıyı yapıştır! @@ -6188,6 +6658,14 @@ Hata: %@ Son mesaj taslağını ekleriyle birlikte koru. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Ön ayarlı sunucu adresi @@ -6223,14 +6701,12 @@ Hata: %@ Gizlilik politikası ve kullanım koşulları. No comment provided by engineer. - - Privacy redefined - Gizlilik yeniden tanımlandı + + Privacy: for owners and subscribers. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. - Özel sohbetler, gruplar ve kişilerinize sunucu operatörleri tarafından erişilemez. + + Private and secure messaging. No comment provided by engineer. @@ -6273,6 +6749,10 @@ Hata: %@ Özel yönlendirme zaman aşımı alert title + + Proceed + alert action + Profile and server connections Profil ve sunucu bağlantıları @@ -6298,9 +6778,8 @@ Hata: %@ Profil teması No comment provided by engineer. - - Profile update will be sent to your contacts. - Profil güncellemesi kişilerinize gönderilecektir. + + Profile update will be sent to your SimpleX contacts. alert message @@ -6308,6 +6787,10 @@ Hata: %@ Sesli/görüntülü aramaları yasakla. No comment provided by engineer. + + Prohibit chats with admins. + No comment provided by engineer. + Prohibit irreversible message deletion. Geri dönüşsüz mesaj silme işlemini yasakla. @@ -6338,6 +6821,10 @@ Hata: %@ Üyelere doğrudan mesaj göndermeyi yasakla. No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + No comment provided by engineer. + Prohibit sending disappearing messages. Kaybolan mesajların gönderimini yasakla. @@ -6405,6 +6892,10 @@ Enable in *Network & servers* settings. Proxy şifre gerektirir No comment provided by engineer. + + Public channels - speak freely 🚀 + No comment provided by engineer. + Push notifications Anında bildirimler @@ -6445,24 +6936,14 @@ Enable in *Network & servers* settings. Dahasını oku No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - [Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + Kullanıcı Rehberinde daha fazlasını okuyun. No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - [Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - No comment provided by engineer. - - - Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - [Kullanıcı Rehberi]nde daha fazlasını okuyun(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 deposu]nda daha fazlasını okuyun(https://github.com/simplex-chat/simplex-chat#readme). + + Read more in our GitHub repository. + GitHub deposunda daha fazlasını okuyun. No comment provided by engineer. @@ -6622,6 +7103,26 @@ swipe action Üyeyi reddet? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + + + Relay results: + alert message + Relay server is only used if necessary. Another party can observe your IP address. Yönlendirici sunucusu yalnızca gerekli olduğunda kullanılır. Başka bir taraf IP adresinizi gözlemleyebilir. @@ -6632,6 +7133,14 @@ swipe action Yönlendirici sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + + + Reliability: many relays per channel. + No comment provided by engineer. + Remove Sil @@ -6671,6 +7180,14 @@ swipe action Anahtar Zinciri'ndeki parola silinsin mi? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. Mesajları kaldırır ve üyeleri engeller. @@ -6906,6 +7423,10 @@ swipe action SOCKS vekili No comment provided by engineer. + + Safe web links + No comment provided by engineer. + Safely receive files Dosyaları güvenle alın @@ -6932,6 +7453,10 @@ chat item action Kaydet (ve üyelere bildir) alert button + + Save (and notify subscribers) + alert button + Save admission settings? Kabul ayarlarını kaydet? @@ -6947,6 +7472,10 @@ chat item action Kaydet ve grup üyelerine bildir No comment provided by engineer. + + Save and notify subscribers + No comment provided by engineer. + Save and reconnect Kayıt et ve yeniden bağlan @@ -6957,6 +7486,14 @@ chat item action Kaydet ve grup profilini güncelle No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Grup profilini kaydet @@ -7132,6 +7669,10 @@ chat item action Güvenlik kodu No comment provided by engineer. + + Security: owners hold channel keys. + No comment provided by engineer. + Select Seç @@ -7262,6 +7803,10 @@ chat item action Mesaj olmadan istek gönder No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. Bunları galeriden veya özel klavyelerden gönder. @@ -7272,6 +7817,10 @@ chat item action Yeni üyelere 100 adete kadar son mesajları gönderin. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + No comment provided by engineer. + Send your private feedback to groups. Özel geri bildiriminizi gruplara gönderin. @@ -7287,6 +7836,10 @@ chat item action Gönderici bağlantı isteğini silmiş olabilir. No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. Görüldü bilgisi, tüm görünür sohbet profillerindeki tüm kişiler için etkinleştirilecektir. @@ -7412,6 +7965,10 @@ chat item action Sunucu protokolü değişti. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin @@ -7542,6 +8099,14 @@ chat item action Ayarlar değiştirildi. alert message + + Setup notifications + No comment provided by engineer. + + + Setup routers + No comment provided by engineer. + Shape profile images Profil resimlerini şekillendir @@ -7578,11 +8143,14 @@ chat item action Adresinizi herkese açık olarak paylaşın No comment provided by engineer. - - Share address with contacts? - Kişilerle adres paylaşılsın mı? + + Share address with SimpleX contacts? alert title + + Share channel + No comment provided by engineer. + Share from other apps. Diğer uygulamalardan paylaşın. @@ -7608,6 +8176,10 @@ chat item action Profil paylaş No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Bu tek kullanımlık bağlantı davetini paylaş @@ -7618,9 +8190,12 @@ chat item action SimpleX ile paylaş No comment provided by engineer. - - Share with contacts - Kişilerle paylaş + + Share via chat + No comment provided by engineer. + + + Share with SimpleX contacts No comment provided by engineer. @@ -7793,9 +8368,8 @@ chat item action SimpleX protokolleri Trail of Bits tarafından incelenmiştir. No comment provided by engineer. - - SimpleX relay link - SimpleX aktarıcı bağlantısı + + SimpleX relay address simplex link type @@ -7871,6 +8445,11 @@ report reason Kare,daire, veya aralarında herhangi bir şey. No comment provided by engineer. + + Star on GitHub + Bize GitHub'da yıldız verin + No comment provided by engineer. + Start chat Sohbeti başlat @@ -7971,6 +8550,63 @@ report reason Abone olundu No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber reports + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers can add message reactions. + No comment provided by engineer. + + + Subscribers can chat with admins. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + No comment provided by engineer. + + + Subscribers can send direct messages. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + No comment provided by engineer. + + + Subscribers can send files and media. + No comment provided by engineer. + + + Subscribers can send voice messages. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Abone olurken hata @@ -8051,6 +8687,10 @@ report reason Fotoğraf çek No comment provided by engineer. + + Talk to someone + No comment provided by engineer. + Tap Connect to chat Sohbet etmek için Bağlan'a dokunun @@ -8066,9 +8706,8 @@ report reason Botu kullanmak için Bağlan tuşuna bas No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. - Daha sonra oluşturmak için menüden BasitX adresi oluştur'a dokunun. + + Tap Join channel No comment provided by engineer. @@ -8101,6 +8740,10 @@ report reason Gizli katılmak için tıkla No comment provided by engineer. + + Tap to open + No comment provided by engineer. + Tap to paste link Bağlantıyı yapıştırmak için tıkla @@ -8119,13 +8762,18 @@ report reason Test failed at step %@. Test %@ adımında başarısız oldu. - server test failure + relay test failure +server test failure Test notifications Bildirimleri test et No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Sunucuyu test et @@ -8178,6 +8826,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Uygulama, her sohbette farklı operatörler kullanarak gizliliğinizi korur. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Uygulama bilinmeyen dosya sunucularından indirmeleri onaylamanızı isteyecektir (.onion hariç). @@ -8193,6 +8845,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Taradığınız kod bir SimpleX bağlantı QR kodu değildir. No comment provided by engineer. + + The connection reached the limit of undelivered messages + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. Bağlantı, teslim edilmemiş mesajlar limitine ulaştı, kişiniz çevrimdışı olabilir. @@ -8218,9 +8874,9 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Şifreleme çalışıyor ve yeni şifreleme anlaşması gerekli değil. Bağlantı hatalarına neden olabilir! No comment provided by engineer. - - The future of messaging - Gizli mesajlaşmanın yeni nesli + + The first network where you own +your contacts and groups. No comment provided by engineer. @@ -8258,6 +8914,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Eski veritabanı geçiş sırasında kaldırılmadı, silinebilir. No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + No comment provided by engineer. + The same conditions will apply to operator **%@**. Aynı koşullar operatör **%@** için de geçerli olacaktır. @@ -8303,6 +8963,14 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Temalar No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + No comment provided by engineer. + These conditions will also apply for: **%@**. Bu koşullar ayrıca şunlar için de geçerli olacaktır: **%@**. @@ -8368,6 +9036,14 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Bu grup artık mevcut değildir. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Bu bağlantı daha yeni bir uygulama sürümü gerektiriyor. Lütfen uygulamayı güncelleyin veya kişinizden uyumlu bir bağlantı göndermesini isteyin. @@ -8418,6 +9094,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. İstenmeyen mesajları gizlemek için. No comment provided by engineer. + + To make SimpleX Network last. + No comment provided by engineer. + To make a new connection Yeni bir bağlantı oluşturmak için @@ -8505,11 +9185,6 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Kişinizle uçtan uca şifrelemeyi doğrulamak için cihazlarınızdaki kodu karşılaştırın (veya tarayın). No comment provided by engineer. - - Toggle chat list: - Sohbet listesini değiştir: - No comment provided by engineer. - Toggle incognito when connecting. Bağlanırken gizli moda geçiş yap. @@ -8525,6 +9200,10 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Araç çubuğu opaklığı No comment provided by engineer. + + Top bar + No comment provided by engineer. + Total Toplam @@ -8589,6 +9268,10 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Üyenin engeli kaldırılsın mı? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Teslim edilmemiş mesajlar @@ -8689,13 +9372,17 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Unsupported connection link Desteklenmeyen bağlantı bağlantısı - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. Yeni üyelere 100e kadar en son mesajlar gönderildi. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + No comment provided by engineer. + Update Güncelle @@ -8821,11 +9508,6 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Sadece ön ayar sunucuları için TCP port 443 kullanın. No comment provided by engineer. - - Use chat - Sohbeti kullan - No comment provided by engineer. - Use current profile Şu anki profili kullan @@ -8841,6 +9523,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Mesajlar için kullan No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Yeni bağlantılar için kullan @@ -8881,6 +9567,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Bilinmeyen sunucularla gizli yönlendirme kullan. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Sunucu kullan @@ -8901,6 +9591,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Uygulamayı tek elle kullan. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + No comment provided by engineer. + Use web port Web portunu kullan @@ -8921,6 +9615,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste SimpleX Chat sunucuları kullanılıyor. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Bilgisayarla kodu doğrula @@ -9040,6 +9738,18 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Sesli mesaj… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + + + Waiting for channel owner to add relays. + No comment provided by engineer. + Waiting for desktop... Bilgisayar için bekleniyor... @@ -9080,6 +9790,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Uyarı: Bazı verileri kaybedebilirsin! No comment provided by engineer. + + We made connecting simpler for new users. + No comment provided by engineer. + WebRTC ICE servers WebRTC ICE sunucuları @@ -9130,6 +9844,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Biriyle gizli bir profil paylaştığınızda, bu profil sizi davet ettikleri gruplar için kullanılacaktır. No comment provided by engineer. + + Why SimpleX is built. + No comment provided by engineer. + WiFi WiFi @@ -9340,6 +10058,10 @@ Katılma isteği tekrarlansın mı? Kilit ekranı bildirim önizlemesini ayarlar üzerinden ayarlayabilirsiniz. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + 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. Bir bağlantı veya QR kodu paylaşabilirsiniz - bu durumda herkes gruba katılabilir. Daha sonra silseniz bile grubun üyelerini kaybetmezsiniz. @@ -9385,16 +10107,21 @@ Katılma isteği tekrarlansın mı? Mesajlar gönderemezsiniz! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Doğrulanamadınız; lütfen tekrar deneyin. No comment provided by engineer. - - You decide who can connect. - Kimin bağlanabileceğine siz karar verirsiniz. - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -9462,6 +10189,10 @@ Bağlantı isteği tekrarlansın mı? Bildirim almanız gerekiyor. token info + + You were born without an account + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. Mesaj gönderebilmek için **isteğinizin kabul edilmesini beklemelisiniz**. @@ -9497,6 +10228,10 @@ Bağlantı isteği tekrarlansın mı? Aktif olduklarında sessize alınmış profillerden arama ve bildirim almaya devam edersiniz. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Bu sohbetten mesaj almaya son vereceksiniz. Sohbet geçmişi korunacaktır. @@ -9542,6 +10277,10 @@ Bağlantı isteği tekrarlansın mı? Aramaların No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Sohbet veritabanınız @@ -9592,6 +10331,10 @@ Bağlantı isteği tekrarlansın mı? Kişileriniz bağlı kalacaktır. No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + No comment provided by engineer. + Your credentials may be sent unencrypted. Kimlik bilgileriniz şifrelenmeden gönderilebilir. @@ -9612,6 +10355,10 @@ Bağlantı isteği tekrarlansın mı? Grubunuz No comment provided by engineer. + + Your network + No comment provided by engineer. + Your preferences Tercihleriniz @@ -9627,6 +10374,11 @@ Bağlantı isteği tekrarlansın mı? Profiliniz No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Profiliniz **%@** paylaşılacaktır. @@ -9647,11 +10399,23 @@ Bağlantı isteği tekrarlansın mı? Profiliniz değiştirildi. Kaydederseniz, güncellenmiş profil tüm kişilerinize gönderilecektir. alert message + + Your public address + No comment provided by engineer. + Your random profile Rasgele profiliniz No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Sunucu adresiniz @@ -9667,21 +10431,11 @@ Bağlantı isteği tekrarlansın mı? Ayarlarınız No comment provided by engineer. - - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - [Katkıda bulun](https://github.com/simplex-chat/simplex-chat#contribute) - No comment provided by engineer. - [Send us email](mailto:chat@simplex.chat) [Bize e-posta gönder](mailto:chat@simplex.chat) No comment provided by engineer. - - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - [Bize GitHub'da yıldız verin](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - \_italic_ \_italik_ @@ -9697,6 +10451,10 @@ Bağlantı isteği tekrarlansın mı? yukarı çıkın, ardından seçin: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ kabul edildi %@ @@ -9717,6 +10475,10 @@ Bağlantı isteği tekrarlansın mı? seni kabul etti rcv group event chat item + + active + No comment provided by engineer. + admin yönetici @@ -9828,6 +10590,10 @@ marked deleted chat item preview text aranıyor… call status + + can't broadcast + No comment provided by engineer. + can't send messages mesaj gönderilemiyor @@ -9863,6 +10629,14 @@ marked deleted chat item preview text adres değiştiriliyor… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored renklendirilmiş @@ -10009,6 +10783,10 @@ pref value silindi deleted chat item + + deleted channel + rcv group event chat item + deleted contact silinmiş kişi @@ -10119,6 +10897,10 @@ pref value hata No comment provided by engineer. + + error: %@ + receive error chat item + expired süresi dolmuş @@ -10248,6 +11030,10 @@ pref value ayrıldı rcv group event chat item + + link + No comment provided by engineer. + marked deleted silinmiş olarak işaretlenmiş @@ -10318,6 +11104,10 @@ pref value asla delete after time + + new + No comment provided by engineer. + new message yeni mesaj @@ -10440,6 +11230,10 @@ time to disappear geri çevrilmiş çağrı call status + + relay + member role + removed kaldırıldı @@ -10450,6 +11244,14 @@ time to disappear %@ kaldırıldı rcv group event chat item + + removed (%d attempts) + receive error chat item + + + removed by operator + No comment provided by engineer. + removed contact address kişi adresi silindi @@ -10604,6 +11406,10 @@ son alınan msj: %2$@ korumasız No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile grup profili güncellendi @@ -10624,6 +11430,10 @@ son alınan msj: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link bağlantı adres uzantısı ile @@ -10699,6 +11509,10 @@ son alınan msj: %2$@ gözlemcisiniz No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ engelledin %@ @@ -10759,6 +11573,10 @@ son alınan msj: %2$@ \~çizik~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + owner verification + 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 9cc95a6085..c20b26e029 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -185,6 +185,21 @@ %d місяців time interval + + %d relays failed + channel relay bar +channel subscriber relay bar + + + %d relays not active + channel relay bar +channel subscriber relay bar + + + %d relays removed + channel relay bar +channel subscriber relay bar + %d sec %d сек @@ -200,11 +215,53 @@ %d пропущено повідомлення(ь) integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d тижнів time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + channel relay bar + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + channel subscriber relay bar + %lld %lld @@ -215,6 +272,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld контакт(и) вибрані @@ -315,11 +376,19 @@ %u повідомлень пропущено. No comment provided by engineer. + + (from owner) + chat link info line + (new) (новий) No comment provided by engineer. + + (signed) + chat link info line + (this device v%@) (цей пристрій v%@) @@ -365,6 +434,10 @@ **Відсканувати / Вставити посилання**: підключитися за отриманим посиланням. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Попередження**: Для отримання миттєвих пуш-сповіщень потрібна парольна фраза, збережена у брелоку. @@ -408,6 +481,12 @@ - і багато іншого! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -506,6 +585,10 @@ time interval Ще кілька речей No comment provided by engineer. + + A link for one person to connect + No comment provided by engineer. + A new contact Новий контакт @@ -632,9 +715,8 @@ swipe action Активні з'єднання 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. - Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Повідомлення про оновлення профілю буде надіслано вашим контактам. + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. @@ -702,6 +784,10 @@ swipe action Додано сервери повідомлень No comment provided by engineer. + + Adding relays will be supported later. + No comment provided by engineer. + Additional accent Додатковий акцент @@ -821,6 +907,14 @@ swipe action Всі профілі profile dropdown + + All relays failed + No comment provided by engineer. + + + All relays removed + No comment provided by engineer. + All reports will be archived for you. Всі скарги будуть заархівовані для вас. @@ -880,6 +974,10 @@ swipe action Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити. (24 години) No comment provided by engineer. + + Allow members to chat with admins. + No comment provided by engineer. + Allow message reactions only if your contact allows them. Дозволяйте реакції на повідомлення, тільки якщо ваш контакт дозволяє їх. @@ -895,6 +993,10 @@ swipe action Дозволяє надсилати прямі повідомлення користувачам. No comment provided by engineer. + + Allow sending direct messages to subscribers. + No comment provided by engineer. + Allow sending disappearing messages. Дозволити надсилання зникаючих повідомлень. @@ -905,6 +1007,10 @@ swipe action Дозволити спільний доступ No comment provided by engineer. + + Allow subscribers to chat with admins. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Дозволяє безповоротно видаляти надіслані повідомлення. (24 години) @@ -1009,11 +1115,6 @@ swipe action Відповісти на дзвінок No comment provided by engineer. - - Anybody can host servers. - Кожен може хостити сервери. - No comment provided by engineer. - App build: %@ Збірка програми: %@ @@ -1218,6 +1319,19 @@ swipe action Поганий хеш повідомлення No comment provided by engineer. + + Be free +in your network + No comment provided by engineer. + + + Be free in your network. + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + No comment provided by engineer. + Better calls Кращі дзвінки @@ -1313,6 +1427,10 @@ swipe action Заблокувати користувача? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Заблокований адміністратором @@ -1361,6 +1479,14 @@ swipe action Надсилати голосові повідомлення можете як ви, так і ваш контакт. No comment provided by engineer. + + Bottom bar + No comment provided by engineer. + + + Broadcast + compose placeholder for channel owner + 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)! @@ -1369,7 +1495,7 @@ swipe action Business address Адреса підприємства - No comment provided by engineer. + chat link info line Business chats @@ -1391,15 +1517,6 @@ swipe action Через профіль чату (за замовчуванням) або [за з'єднанням](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - Використовуючи SimpleX Chat, ви погоджуєтеся: -- надсилати лише легальний контент у публічних групах. -- поважати інших користувачів - без спаму. - No comment provided by engineer. - Call already ended! Дзвінок вже закінчився! @@ -1548,6 +1665,67 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + alert message +alert subtitle + + + Channel image + No comment provided by engineer. + + + Channel link + chat link info line + + + Channel preferences + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel temporarily unavailable + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + + + Channels + No comment provided by engineer. + Chat Чат @@ -1633,6 +1811,22 @@ set passcode view Профіль користувача No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Тема чату @@ -1651,7 +1845,8 @@ set passcode view Chat with admins Чат з адміністраторами - chat toolbar + chat feature +chat toolbar Chat with member @@ -1668,11 +1863,23 @@ set passcode view Чати No comment provided by engineer. + + Chats with admins are prohibited. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + alert message + Chats with members Чати з учасниками No comment provided by engineer. + + Chats with members are disabled + No comment provided by engineer. + Check messages every 20 min. Перевіряйте повідомлення кожні 20 хв. @@ -1683,6 +1890,14 @@ set passcode view Перевірте повідомлення, коли це дозволено. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Перевірте адресу сервера та спробуйте ще раз. @@ -1828,9 +2043,8 @@ set passcode view Налаштування серверів ICE No comment provided by engineer. - - Configure server operators - Налаштувати операторів сервера + + Configure relays No comment provided by engineer. @@ -1891,7 +2105,8 @@ set passcode view Connect Підключіться - server test step + relay test step +server test step Connect automatically @@ -1937,6 +2152,10 @@ This is your own one-time link! Підключіться за посиланням new chat sheet title + + Connect via link or QR code + No comment provided by engineer. + Connect via one-time link Під'єднатися за одноразовим посиланням @@ -2015,7 +2234,7 @@ This is your own one-time link! Connection error (AUTH) Помилка підключення (AUTH) - No comment provided by engineer. + conn error description Connection failed @@ -2073,6 +2292,10 @@ This is your own one-time link! З'єднання No comment provided by engineer. + + Contact address + chat link info line + Contact allows Контакт дозволяє @@ -2142,6 +2365,11 @@ This is your own one-time link! Продовжуйте No comment provided by engineer. + + Contribute + Внесок + No comment provided by engineer. + Conversation deleted! Розмова видалена! @@ -2170,12 +2398,7 @@ This is your own one-time link! Correct name to %@? Виправити ім'я на %@? - No comment provided by engineer. - - - Create - Створити - No comment provided by engineer. + alert message Create 1-time link @@ -2227,6 +2450,14 @@ This is your own one-time link! Створити профіль No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Створити чергу @@ -2237,11 +2468,19 @@ This is your own one-time link! Створіть свою адресу No comment provided by engineer. + + Create your link + No comment provided by engineer. + Create your profile Створіть свій профіль No comment provided by engineer. + + Create your public address + No comment provided by engineer. + Created Створено @@ -2262,6 +2501,10 @@ This is your own one-time link! Створення архівного посилання No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Створення посилання… @@ -2420,10 +2663,9 @@ This is your own one-time link! Доставка налагодження No comment provided by engineer. - - Decentralized - Децентралізований - No comment provided by engineer. + + Decode link + relay test step Decryption error @@ -2471,6 +2713,14 @@ swipe action Видалити та повідомити контакт No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Видалити чат @@ -2640,6 +2890,10 @@ alert button Видалити чергу server test step + + Delete relay + No comment provided by engineer. + Delete report Видалити скаргу @@ -2804,6 +3058,14 @@ alert button У цій групі заборонені прямі повідомлення між учасниками. No comment provided by engineer. + + Direct messages between subscribers are prohibited. + No comment provided by engineer. + + + Disable + alert button + Disable (keep overrides) Вимкнути (зберегти перевизначення) @@ -2909,6 +3171,10 @@ alert button Не надсилайте історію новим користувачам. No comment provided by engineer. + + Do not send history to new subscribers. + No comment provided by engineer. + Do not use credentials with proxy. Не використовуйте облікові дані з проксі. @@ -3010,11 +3276,19 @@ chat item action Зашифровані сповіщення E2E. No comment provided by engineer. + + Easier to invite your friends 👋 + No comment provided by engineer. + Edit Редагувати chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Редагування профілю групи @@ -3028,7 +3302,7 @@ chat item action Enable Увімкнути - No comment provided by engineer. + alert button Enable (keep overrides) @@ -3050,6 +3324,10 @@ chat item action Увімкнути TCP keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Увімкнути автоматичне видалення повідомлень? @@ -3060,6 +3338,10 @@ chat item action Увімкніть доступ до камери No comment provided by engineer. + + Enable chats with admins? + alert title + Enable disappearing messages by default. Увімкнути зникаючі повідомлення за замовчуванням. @@ -3080,16 +3362,15 @@ chat item action Увімкнути миттєві сповіщення? No comment provided by engineer. + + Enable link previews? + alert title + Enable lock Увімкнути блокування No comment provided by engineer. - - Enable notifications - Увімкнути сповіщення - No comment provided by engineer. - Enable periodic notifications? Увімкнути періодичні сповіщення? @@ -3195,6 +3476,10 @@ chat item action Введіть пароль No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Введіть правильну парольну фразу. @@ -3220,6 +3505,14 @@ chat item action Введіть пароль вище, щоб показати! No comment provided by engineer. + + Enter profile name... + No comment provided by engineer. + + + Enter relay name… + No comment provided by engineer. + Enter server manually Увійдіть на сервер вручну @@ -3248,7 +3541,7 @@ chat item action Error Помилка - No comment provided by engineer. + conn error description Error aborting address change @@ -3275,6 +3568,10 @@ chat item action Помилка додавання користувача(ів) No comment provided by engineer. + + Error adding relay + alert title + Error adding server Помилка додавання сервера @@ -3334,6 +3631,10 @@ chat item action Помилка створення адреси No comment provided by engineer. + + Error creating channel + alert title + Error creating group Помилка створення групи @@ -3469,11 +3770,6 @@ chat item action Помилка відкриття чату No comment provided by engineer. - - Error opening group - Помилка відкриття групи - No comment provided by engineer. - Error receiving file Помилка отримання файлу @@ -3519,6 +3815,10 @@ chat item action Помилка збереження серверів ICE No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Помилка під час збереження списку чатів @@ -3583,6 +3883,10 @@ chat item action Помилка встановлення підтвердження доставлення! No comment provided by engineer. + + Error sharing channel + alert title + Error starting chat Помилка запуску чату @@ -3662,7 +3966,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3902,7 +4207,8 @@ snd error text Fingerprint in server address does not match certificate. Відбиток в адресі сервера не співпадає з сертифікатом. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3943,10 +4249,15 @@ snd error text Для всіх модераторів No comment provided by engineer. + + For anyone to reach you + No comment provided by engineer. + For chat profile %@: Для профілю чату %@: - servers error + servers error +servers warning For console @@ -4087,11 +4398,19 @@ Error: %2$@ GIF-файли та наклейки No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. Отримуйте сповіщення, коли вас згадують. No comment provided by engineer. + + Get started + No comment provided by engineer. + Good afternoon! Доброго дня! @@ -4150,7 +4469,7 @@ Error: %2$@ Group link Посилання на групу - No comment provided by engineer. + chat link info line Group links @@ -4262,6 +4581,10 @@ Error: %2$@ Історія не надсилається новим учасникам. No comment provided by engineer. + + History is not sent to new subscribers. + No comment provided by engineer. + How SimpleX works Як працює SimpleX @@ -4360,11 +4683,6 @@ Error: %2$@ Негайно No comment provided by engineer. - - Immune to spam - Імунітет до спаму та зловживань - No comment provided by engineer. - Import Імпорт @@ -4507,9 +4825,9 @@ More improvements are coming soon! Початкова роль 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) + + Install SimpleX Chat for terminal + Встановіть SimpleX Chat для терміналу No comment provided by engineer. @@ -4567,7 +4885,7 @@ More improvements are coming soon! Invalid connection link Неправильне посилання для підключення - No comment provided by engineer. + conn error description Invalid display name! @@ -4587,7 +4905,15 @@ More improvements are coming soon! Invalid name! Неправильне ім'я! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4623,6 +4949,10 @@ More improvements are coming soon! Запросити учасників No comment provided by engineer. + + Invite someone privately + No comment provided by engineer. + Invite to chat Запросити в чат @@ -4699,6 +5029,10 @@ More improvements are coming soon! приєднатися як %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Приєднуйтесь до групи @@ -4786,6 +5120,14 @@ This is your link for group %@! Залишити swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Вийти з чату @@ -4811,6 +5153,10 @@ This is your link for group %@! Менше трафіку в мобільних мережах. No comment provided by engineer. + + Let someone connect to you + No comment provided by engineer. + Let's talk in SimpleX Chat Поговоримо в чаті SimpleX @@ -4831,6 +5177,10 @@ This is your link for group %@! Зв'яжіть мобільні та десктопні додатки! 🔗 No comment provided by engineer. + + Link signature verified. + owner verification + Linked desktop options Параметри пов'язаного робочого столу @@ -5012,6 +5362,10 @@ This is your link for group %@! Учасники групи можуть додавати реакції на повідомлення. No comment provided by engineer. + + Members can chat with admins. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) Учасники групи можуть безповоротно видаляти надіслані повідомлення. (24 години) @@ -5077,6 +5431,10 @@ This is your link for group %@! Чернетка повідомлення No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Повідомлення переслано @@ -5172,6 +5530,14 @@ This is your link for group %@! Повідомлення від %@ будуть показані! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + E2EE info chat item + Messages in this chat will never be deleted. Повідомлення в цьому чаті ніколи не будуть видалені. @@ -5202,16 +5568,15 @@ This is your link for group %@! Повідомлення, файли та дзвінки захищені **квантово-стійким шифруванням e2e** з ідеальною секретністю переадресації, відмовою та відновленням після злому. No comment provided by engineer. + + Migrate + No comment provided by engineer. + Migrate device Перенести пристрій No comment provided by engineer. - - Migrate from another device - Перехід з іншого пристрою - No comment provided by engineer. - Migrate here Мігруйте сюди @@ -5332,6 +5697,10 @@ This is your link for group %@! Мережа та сервери No comment provided by engineer. + + Network commitments + No comment provided by engineer. + Network connection Підключення до мережі @@ -5342,6 +5711,10 @@ This is your link for group %@! Децентралізація мережі No comment provided by engineer. + + Network error + conn error description + Network issues - message expired after many attempts to send it. Проблеми з мережею - термін дії повідомлення закінчився після багатьох спроб надіслати його. @@ -5357,6 +5730,11 @@ This is your link for group %@! Мережевий оператор No comment provided by engineer. + + Network routers cannot know +who talks to whom + No comment provided by engineer. + Network settings Налаштування мережі @@ -5372,6 +5750,10 @@ This is your link for group %@! Новий token status text + + New 1-time link + No comment provided by engineer. + New Passcode Новий пароль @@ -5397,6 +5779,10 @@ This is your link for group %@! Новий досвід спілкування в чаті 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Новий запит на контакт @@ -5467,11 +5853,28 @@ This is your link for group %@! Ні No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + No comment provided by engineer. + + + No active relays + No comment provided by engineer. + No app password Немає пароля програми Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats Без чатів @@ -5617,11 +6020,22 @@ This is your link for group %@! Немає непрочитаних чатів No comment provided by engineer. - - No user identifiers. - Ніяких ідентифікаторів користувачів. + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. + + Non-profit governance + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + No comment provided by engineer. + + + Not all relays connected + alert title + Not compatible! Не сумісні! @@ -5679,7 +6093,7 @@ This is your link for group %@! OK ОК - No comment provided by engineer. + alert button Off @@ -5698,11 +6112,19 @@ new chat action Стара база даних No comment provided by engineer. + + On your phone, not on servers. + No comment provided by engineer. + One-time invitation link Посилання на одноразове запрошення No comment provided by engineer. + + One-time link + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5722,6 +6144,10 @@ Requires compatible VPN. Onion хости не будуть використовуватися. No comment provided by engineer. + + Only channel owners can change channel preferences. + No comment provided by engineer. + Only chat owners can change preferences. Лише власники чату можуть змінювати налаштування. @@ -5823,7 +6249,8 @@ Requires compatible VPN. Open Відкрито - alert action + alert action +alert button Open Settings @@ -5835,6 +6262,10 @@ Requires compatible VPN. Відкриті зміни No comment provided by engineer. + + Open channel + new chat action + Open chat Відкритий чат @@ -5854,6 +6285,10 @@ Requires compatible VPN. Відкриті умови No comment provided by engineer. + + Open external link? + alert title + Open full link alert action @@ -5873,6 +6308,10 @@ Requires compatible VPN. Відкрита міграція на інший пристрій authentication reason + + Open new channel + new chat action + Open new chat Відкрити новий чат @@ -5917,6 +6356,13 @@ Requires compatible VPN. Сервер оператора alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + No comment provided by engineer. + Or import archive file Або імпортуйте архівний файл @@ -5937,6 +6383,10 @@ Requires compatible VPN. Або безпечно поділіться цим посиланням на файл No comment provided by engineer. + + Or show QR in person or via video call. + No comment provided by engineer. + Or show this code Або покажіть цей код @@ -5947,6 +6397,10 @@ Requires compatible VPN. Або поділитися приватно No comment provided by engineer. + + Or use this QR - print or show online. + No comment provided by engineer. + Organize chats into lists Організовуйте чати в списки @@ -5964,6 +6418,18 @@ Requires compatible VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + + + Ownership: you can run your own relays. + No comment provided by engineer. + PING count Кількість PING @@ -6019,6 +6485,10 @@ Requires compatible VPN. Вставити зображення No comment provided by engineer. + + Paste link / Scan + No comment provided by engineer. + Paste link to connect! Вставте посилання для підключення! @@ -6173,6 +6643,14 @@ Error: %@ Зберегти чернетку останнього повідомлення з вкладеннями. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Попередньо встановлена адреса сервера @@ -6208,14 +6686,12 @@ Error: %@ Політика конфіденційності та умови використання. No comment provided by engineer. - - Privacy redefined - Конфіденційність переглянута + + Privacy: for owners and subscribers. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. - Приватні чати, групи та ваші контакти недоступні для операторів сервера. + + Private and secure messaging. No comment provided by engineer. @@ -6258,6 +6734,10 @@ Error: %@ Тайм-аут приватної маршрутизації alert title + + Proceed + alert action + Profile and server connections З'єднання профілю та сервера @@ -6283,9 +6763,8 @@ Error: %@ Тема профілю No comment provided by engineer. - - Profile update will be sent to your contacts. - Оновлення профілю буде надіслано вашим контактам. + + Profile update will be sent to your SimpleX contacts. alert message @@ -6293,6 +6772,10 @@ Error: %@ Заборонити аудіо/відеодзвінки. No comment provided by engineer. + + Prohibit chats with admins. + No comment provided by engineer. + Prohibit irreversible message deletion. Заборонити незворотне видалення повідомлень. @@ -6323,6 +6806,10 @@ Error: %@ Заборонити надсилати прямі повідомлення учасникам. No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + No comment provided by engineer. + Prohibit sending disappearing messages. Заборонити надсилання зникаючих повідомлень. @@ -6390,6 +6877,10 @@ Enable in *Network & servers* settings. Проксі вимагає пароль No comment provided by engineer. + + Public channels - speak freely 🚀 + No comment provided by engineer. + Push notifications Push-сповіщення @@ -6430,24 +6921,14 @@ Enable in *Network & servers* settings. Читати далі No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - Читайте більше в [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + + Read more in User Guide. + Читайте більше в User Guide. No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - 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). + + Read more in our GitHub repository. + Читайте більше в нашому GitHub репозиторії. No comment provided by engineer. @@ -6607,6 +7088,26 @@ swipe action Відхилити учасника? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + + + Relay results: + alert message + Relay server is only used if necessary. Another party can observe your IP address. Релейний сервер використовується тільки в разі потреби. Інша сторона може бачити вашу IP-адресу. @@ -6617,6 +7118,14 @@ swipe action Сервер ретрансляції захищає вашу IP-адресу, але він може спостерігати за тривалістю дзвінка. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + + + Reliability: many relays per channel. + No comment provided by engineer. + Remove Видалити @@ -6655,6 +7164,14 @@ swipe action Видалити парольну фразу з брелока? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. Видаляє повідомлення та блокує користувачів. @@ -6890,6 +7407,10 @@ swipe action Проксі SOCKS No comment provided by engineer. + + Safe web links + No comment provided by engineer. + Safely receive files Безпечне отримання файлів @@ -6916,6 +7437,10 @@ chat item action Зберегти (і повідомити учасникам) alert button + + Save (and notify subscribers) + alert button + Save admission settings? Зберегти налаштування входу? @@ -6931,6 +7456,10 @@ chat item action Зберегти та повідомити учасників групи No comment provided by engineer. + + Save and notify subscribers + No comment provided by engineer. + Save and reconnect Збережіть і підключіться знову @@ -6941,6 +7470,14 @@ chat item action Збереження та оновлення профілю групи No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Зберегти профіль групи @@ -7116,6 +7653,10 @@ chat item action Код безпеки No comment provided by engineer. + + Security: owners hold channel keys. + No comment provided by engineer. + Select Виберіть @@ -7246,6 +7787,10 @@ chat item action Надіслати запит без повідомлення No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. Надсилайте їх із галереї чи власних клавіатур. @@ -7256,6 +7801,10 @@ chat item action Надішліть до 100 останніх повідомлень новим користувачам. No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + No comment provided by engineer. + Send your private feedback to groups. Надсилайте свої приватні відгуки до груп. @@ -7271,6 +7820,10 @@ chat item action Можливо, відправник видалив запит на підключення. No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. Надсилання підтверджень доставки буде ввімкнено для всіх контактів у всіх видимих профілях чату. @@ -7396,6 +7949,10 @@ chat item action Протокол сервера змінено. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Сервер вимагає авторизації для створення черг, перевірте пароль. @@ -7526,6 +8083,14 @@ chat item action Налаштування були змінені. alert message + + Setup notifications + No comment provided by engineer. + + + Setup routers + No comment provided by engineer. + Shape profile images Сформуйте зображення профілю @@ -7562,11 +8127,14 @@ chat item action Поділіться адресою публічно No comment provided by engineer. - - Share address with contacts? - Поділіться адресою з контактами? + + Share address with SimpleX contacts? alert title + + Share channel + No comment provided by engineer. + Share from other apps. Діліться з інших програм. @@ -7592,6 +8160,10 @@ chat item action Поділіться профілем No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Поділіться цим одноразовим посиланням-запрошенням @@ -7602,9 +8174,12 @@ chat item action Поділіться з SimpleX No comment provided by engineer. - - Share with contacts - Поділіться з контактами + + Share via chat + No comment provided by engineer. + + + Share with SimpleX contacts No comment provided by engineer. @@ -7777,8 +8352,8 @@ chat item action Протоколи SimpleX, розглянуті Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7854,6 +8429,11 @@ report reason Квадрат, коло або щось середнє між ними. No comment provided by engineer. + + Star on GitHub + Зірка на GitHub + No comment provided by engineer. + Start chat Почати чат @@ -7954,6 +8534,63 @@ report reason Підписано No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber reports + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers can add message reactions. + No comment provided by engineer. + + + Subscribers can chat with admins. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + No comment provided by engineer. + + + Subscribers can send direct messages. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + No comment provided by engineer. + + + Subscribers can send files and media. + No comment provided by engineer. + + + Subscribers can send voice messages. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Помилки підписки @@ -8034,6 +8671,10 @@ report reason Сфотографуйте No comment provided by engineer. + + Talk to someone + No comment provided by engineer. + Tap Connect to chat Натисніть Підключитися до чату @@ -8048,9 +8689,8 @@ report reason Tap Connect to use bot No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. - Натисніть «Створити адресу SimpleX» у меню, щоб створити її пізніше. + + Tap Join channel No comment provided by engineer. @@ -8083,6 +8723,10 @@ report reason Натисніть, щоб приєднатися інкогніто No comment provided by engineer. + + Tap to open + No comment provided by engineer. + Tap to paste link Натисніть, щоб вставити посилання @@ -8101,13 +8745,18 @@ report reason Test failed at step %@. Тест завершився невдало на кроці %@. - server test failure + relay test failure +server test failure Test notifications Тестові сповіщення No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Тестовий сервер @@ -8160,6 +8809,10 @@ It can happen because of some bug or when the connection is compromised.Додаток захищає вашу конфіденційність, використовуючи різних операторів у кожній розмові. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Програма попросить підтвердити завантаження з невідомих файлових серверів (крім .onion). @@ -8175,6 +8828,10 @@ It can happen because of some bug or when the connection is compromised.Відсканований вами код не є QR-кодом посилання SimpleX. No comment provided by engineer. + + The connection reached the limit of undelivered messages + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. З'єднання досягло ліміту недоставлених повідомлень, ваш контакт може бути офлайн. @@ -8200,9 +8857,9 @@ It can happen because of some bug or when the connection is compromised.Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок з'єднання! No comment provided by engineer. - - The future of messaging - Наступне покоління приватних повідомлень + + The first network where you own +your contacts and groups. No comment provided by engineer. @@ -8240,6 +8897,10 @@ It can happen because of some bug or when the connection is compromised.Стара база даних не була видалена під час міграції, її можна видалити. No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + No comment provided by engineer. + The same conditions will apply to operator **%@**. Такі ж умови діятимуть і для оператора **%@**. @@ -8285,6 +8946,14 @@ It can happen because of some bug or when the connection is compromised.Теми No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + No comment provided by engineer. + These conditions will also apply for: **%@**. Ці умови також поширюються на: **%@**. @@ -8350,6 +9019,14 @@ It can happen because of some bug or when the connection is compromised.Цієї групи більше не існує. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Це посилання вимагає новішої версії додатку. Будь ласка, оновіть додаток або попросіть вашого контакту надіслати сумісне посилання. @@ -8399,6 +9076,10 @@ It can happen because of some bug or when the connection is compromised.Приховати небажані повідомлення. No comment provided by engineer. + + To make SimpleX Network last. + No comment provided by engineer. + To make a new connection Щоб створити нове з'єднання @@ -8485,11 +9166,6 @@ You will be prompted to complete authentication before this feature is enabled.< Щоб перевірити наскрізне шифрування з вашим контактом, порівняйте (або відскануйте) код на ваших пристроях. No comment provided by engineer. - - Toggle chat list: - Перемикання списку чату: - No comment provided by engineer. - Toggle incognito when connecting. Увімкніть інкогніто при підключенні. @@ -8505,6 +9181,10 @@ You will be prompted to complete authentication before this feature is enabled.< Непрозорість панелі інструментів No comment provided by engineer. + + Top bar + No comment provided by engineer. + Total Всього @@ -8569,6 +9249,10 @@ You will be prompted to complete authentication before this feature is enabled.< Розблокувати учасника? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Недоставлені повідомлення @@ -8669,13 +9353,17 @@ To connect, please ask your contact to create another connection link and check Unsupported connection link Несумісне посилання для підключення - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. Новим користувачам надсилається до 100 останніх повідомлень. No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + No comment provided by engineer. + Update Оновлення @@ -8801,11 +9489,6 @@ To connect, please ask your contact to create another connection link and check Використовуйте TCP порт 443 лише для попередньо налаштованих серверів. No comment provided by engineer. - - Use chat - Використовуйте чат - No comment provided by engineer. - Use current profile Використовувати поточний профіль @@ -8821,6 +9504,10 @@ To connect, please ask your contact to create another connection link and check Використовуйте для повідомлень No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Використовуйте для нових з'єднань @@ -8861,6 +9548,10 @@ To connect, please ask your contact to create another connection link and check Використовуйте приватну маршрутизацію з невідомими серверами. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Використовувати сервер @@ -8881,6 +9572,10 @@ To connect, please ask your contact to create another connection link and check Використовуйте додаток однією рукою. No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + No comment provided by engineer. + Use web port Використовувати веб-порт @@ -8901,6 +9596,10 @@ To connect, please ask your contact to create another connection link and check Використання серверів SimpleX Chat. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Перевірте код на робочому столі @@ -9020,6 +9719,18 @@ To connect, please ask your contact to create another connection link and check Голосове повідомлення… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + + + Waiting for channel owner to add relays. + No comment provided by engineer. + Waiting for desktop... Чекаємо на десктопну версію... @@ -9060,6 +9771,10 @@ To connect, please ask your contact to create another connection link and check Попередження: ви можете втратити деякі дані! No comment provided by engineer. + + We made connecting simpler for new users. + No comment provided by engineer. + WebRTC ICE servers Сервери WebRTC ICE @@ -9110,6 +9825,10 @@ To connect, please ask your contact to create another connection link and check Коли ви ділитеся з кимось своїм профілем інкогніто, цей профіль буде використовуватися для груп, до яких вас запрошують. No comment provided by engineer. + + Why SimpleX is built. + No comment provided by engineer. + WiFi WiFi @@ -9320,6 +10039,10 @@ Repeat join request? Ви можете налаштувати попередній перегляд сповіщень на екрані блокування за допомогою налаштувань. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + 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-кодом - будь-хто зможе приєднатися до групи. Ви не втратите учасників групи, якщо згодом видалите її. @@ -9365,16 +10088,21 @@ Repeat join request? Ви не можете надсилати повідомлення! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Вас не вдалося верифікувати, спробуйте ще раз. No comment provided by engineer. - - You decide who can connect. - Ви вирішуєте, хто може під'єднатися. - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -9442,6 +10170,10 @@ Repeat connection request? Ви повинні отримувати сповіщення. token info + + You were born without an account + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. Ви зможете надсилати повідомлення **тільки після того, як ваш запит буде прийнято**. @@ -9477,6 +10209,10 @@ Repeat connection request? Ви все одно отримуватимете дзвінки та сповіщення від вимкнених профілів, якщо вони активні. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Ви більше не будете отримувати повідомлення з цього чату. Історія чату буде збережена. @@ -9522,6 +10258,10 @@ Repeat connection request? Твої дзвінки No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Ваша база даних чату @@ -9572,6 +10312,10 @@ Repeat connection request? Ваші контакти залишаться на зв'язку. No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + No comment provided by engineer. + Your credentials may be sent unencrypted. Ваші облікові дані можуть бути надіслані незашифрованими. @@ -9592,6 +10336,10 @@ Repeat connection request? Ваша група No comment provided by engineer. + + Your network + No comment provided by engineer. + Your preferences Ваші уподобання @@ -9607,6 +10355,11 @@ Repeat connection request? Ваш профіль No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Ваш профіль **%@** буде опублікований. @@ -9627,11 +10380,23 @@ Repeat connection request? Ваш профіль було змінено. Якщо ви збережете його, оновлений профіль буде надіслано всім вашим контактам. alert message + + Your public address + No comment provided by engineer. + Your random profile Ваш випадковий профіль No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Адреса вашого сервера @@ -9647,21 +10412,11 @@ Repeat connection request? Ваші налаштування 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. - \_italic_ \_курсив_ @@ -9677,6 +10432,10 @@ Repeat connection request? вище, а потім обирайте: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ прийнято %@ @@ -9697,6 +10456,10 @@ Repeat connection request? прийняв(ла) вас rcv group event chat item + + active + No comment provided by engineer. + admin адмін @@ -9808,6 +10571,10 @@ marked deleted chat item preview text дзвоніть… call status + + can't broadcast + No comment provided by engineer. + can't send messages не можна надсилати @@ -9843,6 +10610,14 @@ marked deleted chat item preview text змінює адресу… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored кольоровий @@ -9989,6 +10764,10 @@ pref value видалено deleted chat item + + deleted channel + rcv group event chat item + deleted contact видалений контакт @@ -10099,6 +10878,10 @@ pref value помилка No comment provided by engineer. + + error: %@ + receive error chat item + expired закінчився @@ -10228,6 +11011,10 @@ pref value ліворуч rcv group event chat item + + link + No comment provided by engineer. + marked deleted з позначкою видалено @@ -10298,6 +11085,10 @@ pref value ніколи delete after time + + new + No comment provided by engineer. + new message нове повідомлення @@ -10420,6 +11211,10 @@ time to disappear відхилений виклик call status + + relay + member role + removed видалено @@ -10430,6 +11225,14 @@ time to disappear видалено %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + + + removed by operator + No comment provided by engineer. + removed contact address видалено контактну адресу @@ -10582,6 +11385,10 @@ last received msg: %2$@ незахищені No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile оновлений профіль групи @@ -10602,6 +11409,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link за посиланням на контактну адресу @@ -10677,6 +11488,10 @@ last received msg: %2$@ ви спостерігач No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ ви заблокували %@ @@ -10737,6 +11552,10 @@ last received msg: %2$@ \~закреслити~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + owner verification + 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 fbb118774a..51cbb94bda 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 @@ -185,6 +185,21 @@ %d 月 time interval + + %d relays failed + channel relay bar +channel subscriber relay bar + + + %d relays not active + channel relay bar +channel subscriber relay bar + + + %d relays removed + channel relay bar +channel subscriber relay bar + %d sec %d 秒 @@ -200,11 +215,53 @@ 跳过的 %d 条消息 integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d 星期 time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d errors + channel relay bar + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar + + + %1$d/%2$d relays active, %3$d removed + channel relay bar + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d failed + channel subscriber relay bar + + + %1$d/%2$d relays connected, %3$d removed + channel subscriber relay bar + %lld %lld @@ -215,6 +272,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld 联系人已选择 @@ -315,11 +376,19 @@ 已跳过 %u 条消息。 No comment provided by engineer. + + (from owner) + chat link info line + (new) (新) No comment provided by engineer. + + (signed) + chat link info line + (this device v%@) (此设备 v%@) @@ -365,6 +434,10 @@ **扫描/粘贴链接**:用您收到的链接连接。 No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **警告**:及时推送通知需要保存在钥匙串的密码。 @@ -408,6 +481,12 @@ - 以及更多! No comment provided by engineer. + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + No comment provided by engineer. + - optionally notify deleted contacts. - profile names with spaces. @@ -506,6 +585,10 @@ time interval 一些杂项 No comment provided by engineer. + + A link for one person to connect + No comment provided by engineer. + A new contact 新联系人 @@ -632,9 +715,8 @@ swipe action 活动连接 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. - 将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。 + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. @@ -702,6 +784,10 @@ swipe action 已添加消息服务器 No comment provided by engineer. + + Adding relays will be supported later. + No comment provided by engineer. + Additional accent 附加重音 @@ -822,6 +908,14 @@ swipe action 所有配置文件 profile dropdown + + All relays failed + No comment provided by engineer. + + + All relays removed + No comment provided by engineer. + All reports will be archived for you. 将为你存档所有举报。 @@ -882,6 +976,10 @@ swipe action 仅有您的联系人许可后才允许不可撤回消息移除 No comment provided by engineer. + + Allow members to chat with admins. + No comment provided by engineer. + Allow message reactions only if your contact allows them. 只有您的联系人允许时才允许消息回应。 @@ -897,6 +995,10 @@ swipe action 允许向成员发送私信。 No comment provided by engineer. + + Allow sending direct messages to subscribers. + No comment provided by engineer. + Allow sending disappearing messages. 允许发送限时消息。 @@ -907,6 +1009,10 @@ swipe action 允许共享 No comment provided by engineer. + + Allow subscribers to chat with admins. + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) 允许不可撤回地删除已发送消息 @@ -1012,11 +1118,6 @@ swipe action 接听来电 No comment provided by engineer. - - Anybody can host servers. - 任何人都可以托管服务器。 - No comment provided by engineer. - App build: %@ 应用程序构建:%@ @@ -1222,6 +1323,21 @@ swipe action 错误消息散列 No comment provided by engineer. + + Be free +in your network + No comment provided by engineer. + + + Be free in your network. + 在你的网络中自由畅行。 + No comment provided by engineer. + + + Because we destroyed the power to know who you are. So that your power can never be taken. + 因为我们摧毁了知道你是谁的权力,因而您的权利永远不会被夺走。 + No comment provided by engineer. + Better calls 更佳的通话 @@ -1317,6 +1433,10 @@ swipe action 封禁成员吗? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin 由管理员封禁 @@ -1367,6 +1487,14 @@ swipe action 您和您的联系人都可以发送语音消息。 No comment provided by engineer. + + Bottom bar + No comment provided by engineer. + + + Broadcast + compose placeholder for channel owner + 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)! @@ -1375,7 +1503,7 @@ swipe action Business address 企业地址 - No comment provided by engineer. + chat link info line Business chats @@ -1397,15 +1525,6 @@ swipe action 通过聊天资料(默认)或者[通过连接](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)。 No comment provided by engineer. - - By using SimpleX Chat you agree to: -- send only legal content in public groups. -- respect other users – no spam. - 使用 SimpleX Chat 代表您同意: -- 在公开群中只发送合法内容 -- 尊重其他用户 – 没有垃圾信息。 - No comment provided by engineer. - Call already ended! 通话已结束! @@ -1554,6 +1673,67 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel has no active relays. Please try to join later. + alert message +alert subtitle + + + Channel image + No comment provided by engineer. + + + Channel link + chat link info line + + + Channel preferences + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel temporarily unavailable + alert title + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + + + Channels + No comment provided by engineer. + Chat 聊天 @@ -1639,6 +1819,22 @@ set passcode view 用户资料 No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme 聊天主题 @@ -1657,7 +1853,8 @@ set passcode view Chat with admins 和管理员聊天 - chat toolbar + chat feature +chat toolbar Chat with member @@ -1674,11 +1871,23 @@ set passcode view 聊天 No comment provided by engineer. + + Chats with admins are prohibited. + No comment provided by engineer. + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + alert message + Chats with members 和成员聊天 No comment provided by engineer. + + Chats with members are disabled + No comment provided by engineer. + Check messages every 20 min. 每 20 分钟检查消息。 @@ -1689,6 +1898,14 @@ set passcode view 在被允许时检查消息。 No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. 检查服务器地址并再试一次。 @@ -1834,9 +2051,8 @@ set passcode view 配置 ICE 服务器 No comment provided by engineer. - - Configure server operators - 配置服务器运营方 + + Configure relays No comment provided by engineer. @@ -1897,7 +2113,8 @@ set passcode view Connect 连接 - server test step + relay test step +server test step Connect automatically @@ -1943,6 +2160,10 @@ This is your own one-time link! 通过链接连接 new chat sheet title + + Connect via link or QR code + No comment provided by engineer. + Connect via one-time link 通过一次性链接连接 @@ -2021,7 +2242,7 @@ This is your own one-time link! Connection error (AUTH) 连接错误(AUTH) - No comment provided by engineer. + conn error description Connection failed @@ -2078,6 +2299,10 @@ This is your own one-time link! 连接 No comment provided by engineer. + + Contact address + chat link info line + Contact allows 联系人允许 @@ -2148,6 +2373,11 @@ This is your own one-time link! 继续 No comment provided by engineer. + + Contribute + 贡献 + No comment provided by engineer. + Conversation deleted! 对话已删除! @@ -2176,12 +2406,7 @@ This is your own one-time link! Correct name to %@? 将名称更正为 %@? - No comment provided by engineer. - - - Create - 创建 - No comment provided by engineer. + alert message Create 1-time link @@ -2233,6 +2458,14 @@ This is your own one-time link! 创建个人资料 No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue 创建队列 @@ -2243,11 +2476,19 @@ This is your own one-time link! 创建地址 No comment provided by engineer. + + Create your link + No comment provided by engineer. + Create your profile 创建您的资料 No comment provided by engineer. + + Create your public address + No comment provided by engineer. + Created 已创建 @@ -2268,6 +2509,10 @@ This is your own one-time link! 正在创建存档链接 No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… 创建链接中… @@ -2426,10 +2671,9 @@ This is your own one-time link! 调试交付 No comment provided by engineer. - - Decentralized - 分散式 - No comment provided by engineer. + + Decode link + relay test step Decryption error @@ -2477,6 +2721,14 @@ swipe action 删除并通知联系人 No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat 删除聊天 @@ -2647,6 +2899,10 @@ alert button 删除队列 server test step + + Delete relay + No comment provided by engineer. + Delete report 删除举报 @@ -2812,6 +3068,14 @@ alert button 此群禁止成员间私信。 No comment provided by engineer. + + Direct messages between subscribers are prohibited. + No comment provided by engineer. + + + Disable + alert button + Disable (keep overrides) 禁用(保留覆盖) @@ -2917,6 +3181,10 @@ alert button 不给新成员发送历史消息。 No comment provided by engineer. + + Do not send history to new subscribers. + No comment provided by engineer. + Do not use credentials with proxy. 代理不使用身份验证凭据。 @@ -3018,11 +3286,19 @@ chat item action 端到端加密的通知。 No comment provided by engineer. + + Easier to invite your friends 👋 + No comment provided by engineer. + Edit 编辑 chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile 编辑群组资料 @@ -3036,7 +3312,7 @@ chat item action Enable 启用 - No comment provided by engineer. + alert button Enable (keep overrides) @@ -3058,6 +3334,10 @@ chat item action 启用 TCP 保持活跃状态 No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? 启用自动删除消息? @@ -3068,6 +3348,10 @@ chat item action 启用相机访问 No comment provided by engineer. + + Enable chats with admins? + alert title + Enable disappearing messages by default. 默认启用定时消失消息。 @@ -3088,16 +3372,15 @@ chat item action 启用即时通知? No comment provided by engineer. + + Enable link previews? + alert title + Enable lock 启用锁定 No comment provided by engineer. - - Enable notifications - 启用通知 - No comment provided by engineer. - Enable periodic notifications? 启用定期通知? @@ -3203,6 +3486,10 @@ chat item action 输入密码 No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. 输入正确密码。 @@ -3228,6 +3515,14 @@ chat item action 在上面输入密码以显示! No comment provided by engineer. + + Enter profile name... + No comment provided by engineer. + + + Enter relay name… + No comment provided by engineer. + Enter server manually 手动输入服务器 @@ -3256,7 +3551,7 @@ chat item action Error 错误 - No comment provided by engineer. + conn error description Error aborting address change @@ -3283,6 +3578,10 @@ chat item action 添加成员错误 No comment provided by engineer. + + Error adding relay + alert title + Error adding server 添加服务器出错 @@ -3342,6 +3641,10 @@ chat item action 创建地址错误 No comment provided by engineer. + + Error creating channel + alert title + Error creating group 创建群组错误 @@ -3477,11 +3780,6 @@ chat item action 打开聊天时出错 No comment provided by engineer. - - Error opening group - 打开群时出错 - No comment provided by engineer. - Error receiving file 接收文件错误 @@ -3527,6 +3825,10 @@ chat item action 保存 ICE 服务器错误 No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list 保存聊天列表出错 @@ -3592,6 +3894,10 @@ chat item action 设置送达回执出错! No comment provided by engineer. + + Error sharing channel + alert title + Error starting chat 启动聊天错误 @@ -3672,7 +3978,8 @@ snd error text Error: %@. 错误:%@。 - server test error + relay test error +server test error Error: URL is invalid @@ -3916,7 +4223,8 @@ snd error text Fingerprint in server address does not match certificate. 服务器地址中的证书指纹可能不正确 - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3958,10 +4266,15 @@ snd error text 所有 moderators No comment provided by engineer. + + For anyone to reach you + No comment provided by engineer. + For chat profile %@: 为聊天资料 %@: - servers error + servers error +servers warning For console @@ -4102,11 +4415,19 @@ Error: %2$@ GIF 和贴纸 No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. 被提及时收到通知。 No comment provided by engineer. + + Get started + No comment provided by engineer. + Good afternoon! 下午好! @@ -4165,7 +4486,7 @@ Error: %2$@ Group link 群组链接 - No comment provided by engineer. + chat link info line Group links @@ -4277,6 +4598,10 @@ Error: %2$@ 未发送历史消息给新成员。 No comment provided by engineer. + + History is not sent to new subscribers. + No comment provided by engineer. + How SimpleX works SimpleX的工作原理 @@ -4376,11 +4701,6 @@ Error: %2$@ 立即 No comment provided by engineer. - - Immune to spam - 不受垃圾和骚扰消息影响 - No comment provided by engineer. - Import 导入 @@ -4523,9 +4843,9 @@ More improvements are coming soon! 初始角色 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) + + Install SimpleX Chat for terminal + 安装用于终端的 SimpleX Chat No comment provided by engineer. @@ -4583,7 +4903,7 @@ More improvements are coming soon! Invalid connection link 无效的连接链接 - No comment provided by engineer. + conn error description Invalid display name! @@ -4603,7 +4923,15 @@ More improvements are coming soon! Invalid name! 无效名称! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4640,6 +4968,10 @@ More improvements are coming soon! 邀请成员 No comment provided by engineer. + + Invite someone privately + No comment provided by engineer. + Invite to chat 邀请加入聊天 @@ -4716,6 +5048,10 @@ More improvements are coming soon! 以 %@ 身份加入 No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group 加入群组 @@ -4803,6 +5139,14 @@ This is your link for group %@! 离开 swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat 离开聊天 @@ -4828,6 +5172,10 @@ This is your link for group %@! 消耗更少的移动网络数据。 No comment provided by engineer. + + Let someone connect to you + No comment provided by engineer. + Let's talk in SimpleX Chat 让我们一起在 SimpleX Chat 里聊天 @@ -4848,6 +5196,10 @@ This is your link for group %@! 连接移动端和桌面端应用程序!🔗 No comment provided by engineer. + + Link signature verified. + owner verification + Linked desktop options 已链接桌面选项 @@ -5032,6 +5384,10 @@ This is your link for group %@! 群组成员可以添加信息回应。 No comment provided by engineer. + + Members can chat with admins. + No comment provided by engineer. + Members can irreversibly delete sent messages. (24 hours) 群组成员可以不可撤回地删除已发送的消息 @@ -5097,6 +5453,10 @@ This is your link for group %@! 消息草稿 No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded 消息已转发 @@ -5192,6 +5552,14 @@ This is your link for group %@! 将显示来自 %@ 的消息! No comment provided by engineer. + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + No comment provided by engineer. + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + E2EE info chat item + Messages in this chat will never be deleted. 此聊天中的消息永远不会被删除。 @@ -5222,16 +5590,15 @@ This is your link for group %@! 消息、文件和通话受到 **抗量子 e2e 加密** 的保护,具有完全正向保密、否认和闯入恢复。 No comment provided by engineer. + + Migrate + No comment provided by engineer. + Migrate device 迁移设备 No comment provided by engineer. - - Migrate from another device - 从另一台设备迁移 - No comment provided by engineer. - Migrate here 迁移到此处 @@ -5352,6 +5719,10 @@ This is your link for group %@! 网络和服务器 No comment provided by engineer. + + Network commitments + No comment provided by engineer. + Network connection 网络连接 @@ -5362,6 +5733,10 @@ This is your link for group %@! 网络去中心化 No comment provided by engineer. + + Network error + conn error description + Network issues - message expired after many attempts to send it. 网络问题 - 消息在多次尝试发送后过期。 @@ -5377,6 +5752,11 @@ This is your link for group %@! 网络运营方 No comment provided by engineer. + + Network routers cannot know +who talks to whom + No comment provided by engineer. + Network settings 网络设置 @@ -5392,6 +5772,10 @@ This is your link for group %@! token status text + + New 1-time link + No comment provided by engineer. + New Passcode 新密码 @@ -5417,6 +5801,10 @@ This is your link for group %@! 新的聊天体验 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request 新联系人请求 @@ -5487,11 +5875,28 @@ This is your link for group %@! No comment provided by engineer. + + No account. No phone. No email. No ID. +The most secure encryption. + No comment provided by engineer. + + + No active relays + No comment provided by engineer. + No app password 没有应用程序密码 Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats 无聊天 @@ -5637,11 +6042,24 @@ This is your link for group %@! 没有未读聊天 No comment provided by engineer. - - No user identifiers. - 没有用户标识符。 + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. + 没有人追踪你的谈话内容。没有人绘制你去过的地方的地图。隐私从来都不是一项功能--而是一种生活方式。 No comment provided by engineer. + + Non-profit governance + No comment provided by engineer. + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + 别人家的门锁再好也比不上这里。房东再好也比不上这里,他既尊重你的隐私,又保留着所有访客的记录。你不是客人,你是家。没有国王能闯入--你是主人。 + No comment provided by engineer. + + + Not all relays connected + alert title + Not compatible! 不兼容! @@ -5699,7 +6117,7 @@ This is your link for group %@! OK 好的 - No comment provided by engineer. + alert button Off @@ -5718,11 +6136,19 @@ new chat action 旧的数据库 No comment provided by engineer. + + On your phone, not on servers. + No comment provided by engineer. + One-time invitation link 一次性邀请链接 No comment provided by engineer. + + One-time link + chat link info line + Onion hosts will be **required** for connection. Requires compatible VPN. @@ -5742,6 +6168,10 @@ Requires compatible VPN. 将不会使用 Onion 主机。 No comment provided by engineer. + + Only channel owners can change channel preferences. + No comment provided by engineer. + Only chat owners can change preferences. 仅聊天所有人可更改首选项。 @@ -5845,7 +6275,8 @@ Requires compatible VPN. Open 打开 - alert action + alert action +alert button Open Settings @@ -5857,6 +6288,10 @@ Requires compatible VPN. 打开更改 No comment provided by engineer. + + Open channel + new chat action + Open chat 打开聊天 @@ -5877,6 +6312,10 @@ Requires compatible VPN. 打开条款 No comment provided by engineer. + + Open external link? + alert title + Open full link 打开完整链接 @@ -5897,6 +6336,10 @@ Requires compatible VPN. 打开迁移到另一台设备 authentication reason + + Open new channel + new chat action + Open new chat 打开新聊天 @@ -5942,6 +6385,13 @@ Requires compatible VPN. 运营方服务器 alert title + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + No comment provided by engineer. + Or import archive file 或者导入或者导入压缩文件 @@ -5962,6 +6412,10 @@ Requires compatible VPN. 或安全地分享此文件链接 No comment provided by engineer. + + Or show QR in person or via video call. + No comment provided by engineer. + Or show this code 或者显示此码 @@ -5972,6 +6426,10 @@ Requires compatible VPN. 或者私下分享 No comment provided by engineer. + + Or use this QR - print or show online. + No comment provided by engineer. + Organize chats into lists 将聊天组织到列表 @@ -5989,6 +6447,18 @@ Requires compatible VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + + + Ownership: you can run your own relays. + No comment provided by engineer. + PING count PING 次数 @@ -6044,6 +6514,10 @@ Requires compatible VPN. 粘贴图片 No comment provided by engineer. + + Paste link / Scan + No comment provided by engineer. + Paste link to connect! 粘贴链接以连接! @@ -6198,6 +6672,14 @@ Error: %@ 保留最后的消息草稿及其附件。 No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address 预设服务器地址 @@ -6233,14 +6715,12 @@ Error: %@ 隐私政策和使用条款。 No comment provided by engineer. - - Privacy redefined - 重新定义隐私 + + Privacy: for owners and subscribers. No comment provided by engineer. - - Private chats, groups and your contacts are not accessible to server operators. - 服务器运营方无法访问私密聊天、群组和你的联系人。 + + Private and secure messaging. No comment provided by engineer. @@ -6283,6 +6763,10 @@ Error: %@ 私密路由超时 alert title + + Proceed + alert action + Profile and server connections 资料和服务器连接 @@ -6308,9 +6792,8 @@ Error: %@ 个人资料主题 No comment provided by engineer. - - Profile update will be sent to your contacts. - 个人资料更新将被发送给您的联系人。 + + Profile update will be sent to your SimpleX contacts. alert message @@ -6318,6 +6801,10 @@ Error: %@ 禁止音频/视频通话。 No comment provided by engineer. + + Prohibit chats with admins. + No comment provided by engineer. + Prohibit irreversible message deletion. 禁止不可撤回消息删除。 @@ -6348,6 +6835,10 @@ Error: %@ 禁止向成员发送私信。 No comment provided by engineer. + + Prohibit sending direct messages to subscribers. + No comment provided by engineer. + Prohibit sending disappearing messages. 禁止发送限时消息。 @@ -6415,6 +6906,10 @@ Enable in *Network & servers* settings. 代理需要密码 No comment provided by engineer. + + Public channels - speak freely 🚀 + No comment provided by engineer. + Push notifications 推送通知 @@ -6455,24 +6950,14 @@ Enable in *Network & servers* settings. 阅读更多 No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - 阅读更多[User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)。 + + Read more in User Guide. + 阅读更多User Guide。 No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). - 在 [用户指南](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) 中阅读更多内容。 - 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) 中阅读更多信息。 + + Read more in our GitHub repository. + 在我们的 GitHub 仓库 中阅读更多信息。 No comment provided by engineer. @@ -6631,6 +7116,26 @@ swipe action 拒绝成员? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + + + Relay results: + alert message + Relay server is only used if necessary. Another party can observe your IP address. 中继服务器仅在必要时使用。其他人可能会观察到您的IP地址。 @@ -6641,6 +7146,14 @@ swipe action 中继服务器保护您的 IP 地址,但它可以观察通话的持续时间。 No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + + + Reliability: many relays per channel. + No comment provided by engineer. + Remove 移除 @@ -6681,6 +7194,14 @@ swipe action 从钥匙串中删除密码? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. 删除消息并封禁成员。 @@ -6915,6 +7436,10 @@ swipe action SOCKS代理 No comment provided by engineer. + + Safe web links + No comment provided by engineer. + Safely receive files 安全接收文件 @@ -6941,6 +7466,10 @@ chat item action 保存(并通知成员) alert button + + Save (and notify subscribers) + alert button + Save admission settings? 保存入群设置? @@ -6956,6 +7485,10 @@ chat item action 保存并通知群组成员 No comment provided by engineer. + + Save and notify subscribers + No comment provided by engineer. + Save and reconnect 保存并重新连接 @@ -6966,6 +7499,14 @@ chat item action 保存和更新组配置文件 No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile 保存群组资料 @@ -7146,6 +7687,10 @@ chat item action 安全码 No comment provided by engineer. + + Security: owners hold channel keys. + No comment provided by engineer. + Select 选择 @@ -7276,6 +7821,10 @@ chat item action 发送无消息请求 No comment provided by engineer. + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + No comment provided by engineer. + Send them from gallery or custom keyboards. 发送它们来自图库或自定义键盘。 @@ -7286,6 +7835,10 @@ chat item action 给新成员发送最多 100 条历史消息。 No comment provided by engineer. + + Send up to 100 last messages to new subscribers. + No comment provided by engineer. + Send your private feedback to groups. 向群发送私密反馈。 @@ -7301,6 +7854,10 @@ chat item action 发送人可能已删除连接请求。 No comment provided by engineer. + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + alert message + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. 将对所有可见聊天配置文件中的所有联系人启用送达回执功能。 @@ -7426,6 +7983,10 @@ chat item action 服务器协议已更改。 alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. 服务器需要授权才能创建队列,检查密码 @@ -7556,6 +8117,14 @@ chat item action 设置已修改。 alert message + + Setup notifications + No comment provided by engineer. + + + Setup routers + No comment provided by engineer. + Shape profile images 改变个人资料图形状 @@ -7592,11 +8161,14 @@ chat item action 公开分享地址 No comment provided by engineer. - - Share address with contacts? - 与联系人分享地址? + + Share address with SimpleX contacts? alert title + + Share channel + No comment provided by engineer. + Share from other apps. 从其他应用程序共享。 @@ -7622,6 +8194,10 @@ chat item action 分享资料 No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link 分享此一次性邀请链接 @@ -7632,9 +8208,12 @@ chat item action 分享到 SimpleX No comment provided by engineer. - - Share with contacts - 与联系人分享 + + Share via chat + No comment provided by engineer. + + + Share with SimpleX contacts No comment provided by engineer. @@ -7807,9 +8386,8 @@ chat item action SimpleX 协议由 Trail of Bits 审阅。 No comment provided by engineer. - - SimpleX relay link - SimpleX 中继链接 + + SimpleX relay address simplex link type @@ -7885,6 +8463,11 @@ report reason 方形、圆形、或两者之间的任意形状. No comment provided by engineer. + + Star on GitHub + 在 GitHub 上加星 + No comment provided by engineer. + Start chat 开始聊天 @@ -7984,6 +8567,63 @@ report reason 已订阅 No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber reports + chat feature + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers can add message reactions. + No comment provided by engineer. + + + Subscribers can chat with admins. + No comment provided by engineer. + + + Subscribers can irreversibly delete sent messages. (24 hours) + No comment provided by engineer. + + + Subscribers can report messsages to moderators. + No comment provided by engineer. + + + Subscribers can send SimpleX links. + No comment provided by engineer. + + + Subscribers can send direct messages. + No comment provided by engineer. + + + Subscribers can send disappearing messages. + No comment provided by engineer. + + + Subscribers can send files and media. + No comment provided by engineer. + + + Subscribers can send voice messages. + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors 订阅错误 @@ -8064,6 +8704,10 @@ report reason 拍照 No comment provided by engineer. + + Talk to someone + No comment provided by engineer. + Tap Connect to chat 轻按连接进行聊天 @@ -8079,9 +8723,8 @@ report reason 轻按“连接”使用机器人 No comment provided by engineer. - - Tap Create SimpleX address in the menu to create it later. - 要稍后创建 SimpleX 地址,请在菜单中轻按“创建 SimpleX 地址” + + Tap Join channel No comment provided by engineer. @@ -8114,6 +8757,10 @@ report reason 点击以加入隐身聊天 No comment provided by engineer. + + Tap to open + No comment provided by engineer. + Tap to paste link 轻按粘贴链接 @@ -8132,13 +8779,18 @@ report reason Test failed at step %@. 在步骤 %@ 上测试失败。 - server test failure + relay test failure +server test failure Test notifications 测试通知 No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server 测试服务器 @@ -8191,6 +8843,10 @@ It can happen because of some bug or when the connection is compromised.应用通过在每个对话中使用不同运营方保护你的隐私。 No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). 该应用程序将要求确认从未知文件服务器(.onion 除外)下载。 @@ -8206,6 +8862,10 @@ It can happen because of some bug or when the connection is compromised.您扫描的码不是 SimpleX 链接的二维码。 No comment provided by engineer. + + The connection reached the limit of undelivered messages + conn error description + The connection reached the limit of undelivered messages, your contact may be offline. 连接达到了未送达消息上限,你的联系人可能处于离线状态。 @@ -8231,9 +8891,9 @@ It can happen because of some bug or when the connection is compromised.加密正在运行,不需要新的加密协议。这可能会导致连接错误! No comment provided by engineer. - - The future of messaging - 下一代私密通讯软件 + + The first network where you own +your contacts and groups. No comment provided by engineer. @@ -8271,6 +8931,11 @@ It can happen because of some bug or when the connection is compromised.旧数据库在迁移过程中没有被移除,可以删除。 No comment provided by engineer. + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + 人类最古老的自由--与他人交谈而不被监视--建立在不会背叛它的基础设施之上。 + No comment provided by engineer. + The same conditions will apply to operator **%@**. No comment provided by engineer. @@ -8314,6 +8979,16 @@ It can happen because of some bug or when the connection is compromised.主题 No comment provided by engineer. + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + 然后我们转向线上,每个平台都要求你提供一些信息--你的姓名、电话号码、好友列表。我们接受了这样一个事实:与人交流的代价就是让别人知道我们在和谁交流。每一代人,每一代科技,都遵循着这样的模式--电话、电子邮件、即时通讯、社交媒体。这似乎是唯一可行的方式。 + No comment provided by engineer. + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + 还有另一种方法。一个没有电话号码、没有用户名、没有账户、没有任何用户身份的网络。一个连接人们并传输加密信息的网络,而无需知道谁连接了。 + No comment provided by engineer. + These conditions will also apply for: **%@**. 这些条件将同样适用于: **%@**。 @@ -8379,6 +9054,14 @@ It can happen because of some bug or when the connection is compromised.该群组已不存在。 No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. 此链接需要更新的应用版本。请升级应用或请求你的联系人发送相容的链接。 @@ -8429,6 +9112,10 @@ It can happen because of some bug or when the connection is compromised.隐藏不需要的信息。 No comment provided by engineer. + + To make SimpleX Network last. + No comment provided by engineer. + To make a new connection 建立新连接 @@ -8516,11 +9203,6 @@ You will be prompted to complete authentication before this feature is enabled.< 要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。 No comment provided by engineer. - - Toggle chat list: - 切换聊天列表: - No comment provided by engineer. - Toggle incognito when connecting. 在连接时切换隐身模式。 @@ -8535,6 +9217,10 @@ You will be prompted to complete authentication before this feature is enabled.< 工具栏不透明度 No comment provided by engineer. + + Top bar + No comment provided by engineer. + Total 共计 @@ -8600,6 +9286,10 @@ You will be prompted to complete authentication before this feature is enabled.< 解封成员吗? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages 未送达的消息 @@ -8700,13 +9390,17 @@ To connect, please ask your contact to create another connection link and check Unsupported connection link 不支持的连接链接 - No comment provided by engineer. + conn error description Up to 100 last messages are sent to new members. 给新成员发送了最多 100 条历史消息。 No comment provided by engineer. + + Up to 100 last messages are sent to new subscribers. + No comment provided by engineer. + Update 更新 @@ -8832,11 +9526,6 @@ To connect, please ask your contact to create another connection link and check 仅预设服务器使用 TCP 协议 443 端口。 No comment provided by engineer. - - Use chat - 使用聊天 - No comment provided by engineer. - Use current profile 使用当前配置文件 @@ -8852,6 +9541,10 @@ To connect, please ask your contact to create another connection link and check 用于消息 No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections 用于新连接 @@ -8892,6 +9585,10 @@ To connect, please ask your contact to create another connection link and check 对未知服务器使用私有路由。 No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server 使用服务器 @@ -8912,6 +9609,10 @@ To connect, please ask your contact to create another connection link and check 用一只手使用应用程序。 No comment provided by engineer. + + Use this address in your social media profile, website, or email signature. + No comment provided by engineer. + Use web port 使用 web 端口 @@ -8932,6 +9633,10 @@ To connect, please ask your contact to create another connection link and check 使用 SimpleX Chat 服务器。 No comment provided by engineer. + + Verify + relay test step + Verify code with desktop 用桌面端验证代码 @@ -9052,6 +9757,18 @@ To connect, please ask your contact to create another connection link and check 语音消息…… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + + + Waiting for channel owner to add relays. + No comment provided by engineer. + Waiting for desktop... 正在等待桌面... @@ -9092,6 +9809,10 @@ To connect, please ask your contact to create another connection link and check 警告:您可能会丢失部分数据! No comment provided by engineer. + + We made connecting simpler for new users. + No comment provided by engineer. + WebRTC ICE servers WebRTC ICE 服务器 @@ -9142,6 +9863,10 @@ To connect, please ask your contact to create another connection link and check 当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。 No comment provided by engineer. + + Why SimpleX is built. + No comment provided by engineer. + WiFi WiFi @@ -9354,6 +10079,10 @@ Repeat join request? 您可以通过设置来设置锁屏通知预览。 No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + 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. 您可以共享链接或二维码——任何人都可以加入该群组。如果您稍后将其删除,您不会失去该组的成员。 @@ -9399,16 +10128,21 @@ Repeat join request? 您无法发送消息! alert title + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + No comment provided by engineer. + + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. 您的身份无法验证,请再试一次。 No comment provided by engineer. - - You decide who can connect. - 你决定谁可以连接。 - No comment provided by engineer. - You have already requested connection! Repeat connection request? @@ -9475,6 +10209,11 @@ Repeat connection request? You should receive notifications. token info + + You were born without an account + 你生来就没有账户。 + No comment provided by engineer. + You will be able to send messages **only after your request is accepted**. **只有在你的请求被接受后**你才能发送消息。 @@ -9510,6 +10249,10 @@ Repeat connection request? 当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。 No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. 你将停止从这个聊天收到消息。聊天历史将被保留。 @@ -9555,6 +10298,10 @@ Repeat connection request? 您的通话 No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database 您的聊天数据库 @@ -9603,6 +10350,11 @@ Repeat connection request? 与您的联系人保持连接。 No comment provided by engineer. + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + 你的对话内容始终属于你,就像互联网出现之前一样。网络不是一个你访问的地方,而是一个你创建并拥有的地方。无论你将其设为私密还是公开,任何人都无法将其夺走。 + No comment provided by engineer. + Your credentials may be sent unencrypted. 你的凭据可能以未经加密的方式被发送。 @@ -9623,6 +10375,10 @@ Repeat connection request? 你的群 No comment provided by engineer. + + Your network + No comment provided by engineer. + Your preferences 您的偏好设置 @@ -9638,6 +10394,11 @@ Repeat connection request? 您的个人资料 No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. 您的个人资料 **%@** 将被共享。 @@ -9658,11 +10419,23 @@ Repeat connection request? 您的个人资料已修改。如果进行保存,更新后的个人资料将发送到所有联系人。 alert message + + Your public address + No comment provided by engineer. + Your random profile 您的随机资料 No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address 您的服务器地址 @@ -9678,21 +10451,11 @@ Repeat connection request? 您的设置 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. - \_italic_ \_斜体_ @@ -9708,6 +10471,10 @@ Repeat connection request? 上面,然后选择: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ rcv group event chat item @@ -9727,6 +10494,10 @@ Repeat connection request? 接受了你 rcv group event chat item + + active + No comment provided by engineer. + admin 管理员 @@ -9838,6 +10609,10 @@ marked deleted chat item preview text 呼叫中…… call status + + can't broadcast + No comment provided by engineer. + can't send messages 无法发送消息 @@ -9873,6 +10648,14 @@ marked deleted chat item preview text 更改地址… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored 彩色 @@ -10019,6 +10802,10 @@ pref value 已删除 deleted chat item + + deleted channel + rcv group event chat item + deleted contact 已删除联系人 @@ -10129,6 +10916,10 @@ pref value 错误 No comment provided by engineer. + + error: %@ + receive error chat item + expired 过期 @@ -10258,6 +11049,10 @@ pref value 已离开 rcv group event chat item + + link + No comment provided by engineer. + marked deleted 标记为已删除 @@ -10328,6 +11123,10 @@ pref value 从不 delete after time + + new + No comment provided by engineer. + new message 新消息 @@ -10450,6 +11249,10 @@ time to disappear 拒接来电 call status + + relay + member role + removed 已删除 @@ -10460,6 +11263,14 @@ time to disappear 已删除 %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + + + removed by operator + No comment provided by engineer. + removed contact address 删除了联系地址 @@ -10614,6 +11425,10 @@ last received msg: %2$@ 未受保护 No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile 已更新的群组资料 @@ -10634,6 +11449,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link 通过联系地址链接 @@ -10709,6 +11528,10 @@ last received msg: %2$@ 您是观察者 No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ 你阻止了%@ @@ -10769,6 +11592,10 @@ last received msg: %2$@ \~删去~ No comment provided by engineer. + + ⚠️ Signature verification failed: %@. + owner verification + diff --git a/apps/ios/SimpleX SE/ShareAPI.swift b/apps/ios/SimpleX SE/ShareAPI.swift index f13401d437..52c0405e5e 100644 --- a/apps/ios/SimpleX SE/ShareAPI.swift +++ b/apps/ios/SimpleX SE/ShareAPI.swift @@ -68,7 +68,7 @@ func apiSendMessages( type: chatInfo.chatType, id: chatInfo.apiId, scope: chatInfo.groupChatScope(), - sendAsGroup: chatInfo.groupInfo.map { $0.useRelays && $0.membership.memberRole >= .owner } ?? false, + sendAsGroup: chatInfo.sendAsGroup, live: false, ttl: nil, composedMessages: composedMessages diff --git a/apps/ios/SimpleX SE/ShareModel.swift b/apps/ios/SimpleX SE/ShareModel.swift index fd5c4c990f..18f3e2c344 100644 --- a/apps/ios/SimpleX SE/ShareModel.swift +++ b/apps/ios/SimpleX SE/ShareModel.swift @@ -465,8 +465,7 @@ fileprivate func getSharedContent(_ ip: NSItemProvider) async -> Result String { switch self { case .timedMessages: return NSLocalizedString("Disappearing messages", comment: "chat feature") case .directMessages: return NSLocalizedString("Direct messages", comment: "chat feature") @@ -900,8 +904,11 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { case .voice: return NSLocalizedString("Voice messages", comment: "chat feature") case .files: return NSLocalizedString("Files and media", comment: "chat feature") case .simplexLinks: return NSLocalizedString("SimpleX links", comment: "chat feature") - case .reports: return NSLocalizedString("Member reports", comment: "chat feature") + case .reports: return isChannel + ? NSLocalizedString("Subscriber reports", comment: "chat feature") + : NSLocalizedString("Member reports", comment: "chat feature") case .history: return NSLocalizedString("Visible history", comment: "chat feature") + case .support: return NSLocalizedString("Chat with admins", comment: "chat feature") } } @@ -916,6 +923,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { case .simplexLinks: return "link.circle" case .reports: return "flag" case .history: return "clock" + case .support: return "questionmark.circle" } } @@ -930,6 +938,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { case .simplexLinks: return "link.circle.fill" case .reports: return "flag.fill" case .history: return "clock.fill" + case .support: return "questionmark.circle.fill" } } @@ -940,7 +949,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { } } - public func enableDescription(_ enabled: GroupFeatureEnabled, _ canEdit: Bool) -> LocalizedStringKey { + public func enableDescription(_ enabled: GroupFeatureEnabled, _ canEdit: Bool, isChannel: Bool = false) -> LocalizedStringKey { if canEdit { switch self { case .timedMessages: @@ -950,8 +959,12 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { } case .directMessages: switch enabled { - case .on: return "Allow sending direct messages to members." - case .off: return "Prohibit sending direct messages to members." + case .on: return isChannel + ? "Allow sending direct messages to subscribers." + : "Allow sending direct messages to members." + case .off: return isChannel + ? "Prohibit sending direct messages to subscribers." + : "Prohibit sending direct messages to members." } case .fullDelete: switch enabled { @@ -985,56 +998,96 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { } case .history: switch enabled { - case .on: return "Send up to 100 last messages to new members." - case .off: return "Do not send history to new members." + case .on: return isChannel + ? "Send up to 100 last messages to new subscribers." + : "Send up to 100 last messages to new members." + case .off: return isChannel + ? "Do not send history to new subscribers." + : "Do not send history to new members." + } + case .support: + switch enabled { + case .on: return isChannel + ? "Allow subscribers to chat with admins." + : "Allow members to chat with admins." + case .off: return "Prohibit chats with admins." } } } else { switch self { case .timedMessages: switch enabled { - case .on: return "Members can send disappearing messages." + case .on: return isChannel + ? "Subscribers can send disappearing messages." + : "Members can send disappearing messages." case .off: return "Disappearing messages are prohibited." } case .directMessages: switch enabled { - case .on: return "Members can send direct messages." - case .off: return "Direct messages between members are prohibited." + case .on: return isChannel + ? "Subscribers can send direct messages." + : "Members can send direct messages." + case .off: return isChannel + ? "Direct messages between subscribers are prohibited." + : "Direct messages between members are prohibited." } case .fullDelete: switch enabled { - case .on: return "Members can irreversibly delete sent messages. (24 hours)" + case .on: return isChannel + ? "Subscribers can irreversibly delete sent messages. (24 hours)" + : "Members can irreversibly delete sent messages. (24 hours)" case .off: return "Irreversible message deletion is prohibited." } case .reactions: switch enabled { - case .on: return "Members can add message reactions." + case .on: return isChannel + ? "Subscribers can add message reactions." + : "Members can add message reactions." case .off: return "Message reactions are prohibited." } case .voice: switch enabled { - case .on: return "Members can send voice messages." + case .on: return isChannel + ? "Subscribers can send voice messages." + : "Members can send voice messages." case .off: return "Voice messages are prohibited." } case .files: switch enabled { - case .on: return "Members can send files and media." + case .on: return isChannel + ? "Subscribers can send files and media." + : "Members can send files and media." case .off: return "Files and media are prohibited." } case .simplexLinks: switch enabled { - case .on: return "Members can send SimpleX links." + case .on: return isChannel + ? "Subscribers can send SimpleX links." + : "Members can send SimpleX links." case .off: return "SimpleX links are prohibited." } case .reports: switch enabled { - case .on: return "Members can report messsages to moderators." + case .on: return isChannel + ? "Subscribers can report messsages to moderators." + : "Members can report messsages to moderators." case .off: return "Reporting messages to moderators is prohibited." } case .history: switch enabled { - case .on: return "Up to 100 last messages are sent to new members." - case .off: return "History is not sent to new members." + case .on: return isChannel + ? "Up to 100 last messages are sent to new subscribers." + : "Up to 100 last messages are sent to new members." + case .off: return isChannel + ? "History is not sent to new subscribers." + : "History is not sent to new members." + } + case .support: + switch enabled { + case .on: return isChannel + ? "Subscribers can chat with admins." + : "Members can chat with admins." + case .off: return "Chats with admins are prohibited." } } } @@ -1190,6 +1243,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { public var simplexLinks: RoleGroupPreference public var reports: GroupPreference public var history: GroupPreference + public var support: GroupPreference public var commands: [ChatBotCommand] public init( @@ -1202,6 +1256,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { simplexLinks: RoleGroupPreference, reports: GroupPreference, history: GroupPreference, + support: GroupPreference, commands: [ChatBotCommand] ) { self.timedMessages = timedMessages @@ -1213,6 +1268,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { self.simplexLinks = simplexLinks self.reports = reports self.history = history + self.support = support self.commands = commands } @@ -1226,6 +1282,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { simplexLinks: RoleGroupPreference(enable: .on, role: nil), reports: GroupPreference(enable: .on), history: GroupPreference(enable: .on), + support: GroupPreference(enable: .on), commands: [] ) } @@ -1240,6 +1297,7 @@ public struct GroupPreferences: Codable, Hashable { public var simplexLinks: RoleGroupPreference? public var reports: GroupPreference? public var history: GroupPreference? + public var support: GroupPreference? public var commands: [ChatBotCommand]? public init( @@ -1252,6 +1310,7 @@ public struct GroupPreferences: Codable, Hashable { simplexLinks: RoleGroupPreference? = nil, reports: GroupPreference? = nil, history: GroupPreference? = nil, + support: GroupPreference? = nil, commands: [ChatBotCommand]? = nil ) { self.timedMessages = timedMessages @@ -1263,6 +1322,7 @@ public struct GroupPreferences: Codable, Hashable { self.simplexLinks = simplexLinks self.reports = reports self.history = history + self.support = support self.commands = commands } @@ -1276,6 +1336,7 @@ public struct GroupPreferences: Codable, Hashable { simplexLinks: RoleGroupPreference(enable: .on, role: nil), reports: GroupPreference(enable: .on), history: GroupPreference(enable: .on), + support: GroupPreference(enable: .on), commands: nil ) } @@ -1563,8 +1624,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } } - public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { - get { + public func userCantSendReason(allRelaysBroken: Bool = false) -> (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { switch self { case let .direct(contact): if contact.sendMsgToConnect { return nil } @@ -1578,6 +1638,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { if groupInfo.membership.memberActive { switch(groupChatScope) { case .none: + if allRelaysBroken && groupInfo.useRelays { return ("can't broadcast", nil) } if groupInfo.membership.memberPending { return ("reviewed by admins", "Please contact group admin.") } if groupInfo.membership.memberRole == .observer { return groupInfo.useRelays ? ("you are subscriber", nil) : ("you are observer", "Please contact group admin.") @@ -1613,10 +1674,9 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case .invalidJSON: return ("can't send messages", nil) } - } } - public var sendMsgEnabled: Bool { userCantSendReason == nil } + public var sendMsgEnabled: Bool { userCantSendReason() == nil } public var incognito: Bool { get { @@ -1652,6 +1712,10 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } } + public var isChannel: Bool { + groupInfo?.useRelays == true + } + // this works for features that are common for contacts and groups public func featureEnabled(_ feature: ChatFeature) -> Bool { switch self { @@ -1757,6 +1821,18 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } } + public var sendAsGroup: Bool { + if let g = groupInfo, g.useRelays && g.membership.memberRole >= .owner { + switch groupChatScope() { + case .none: true + case .memberSupport: false + case .reports: false + } + } else { + false + } + } + public func ntfsEnabled(chatItem: ChatItem) -> Bool { ntfsEnabled(chatItem.meta.userMention) } @@ -2368,6 +2444,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { public var ready: Bool { get { true } } public var nextConnectPrepared: Bool { if let preparedGroup { !preparedGroup.connLinkStartedConnection } else { false } } public var profileChangeProhibited: Bool { preparedGroup?.connLinkPreparedConnection ?? false } + public var isChannel: Bool { groupProfile.isChannel } public var displayName: String { localAlias == "" ? groupProfile.displayName : localAlias } public var fullName: String { get { groupProfile.fullName } } public var shortDescr: String? { groupProfile.shortDescr } @@ -2496,6 +2573,8 @@ public struct GroupProfile: Codable, NamedChat, Hashable { set { memberAdmission = newValue } } + public var isChannel: Bool { publicGroup?.groupType == .channel } + public static let sampleData = GroupProfile( displayName: "team", fullName: "My Team" @@ -2560,6 +2639,7 @@ public enum RelayStatus: String, Decodable, Equatable, Hashable { case rsInvited = "invited" case rsAccepted = "accepted" case rsActive = "active" + case rsInactive = "inactive" } public struct RelayProfile: Codable, Equatable, Hashable { @@ -2632,6 +2712,7 @@ extension RelayStatus { case .rsInvited: "invited" case .rsAccepted: "accepted" case .rsActive: "active" + case .rsInactive: "inactive" } } } @@ -3191,11 +3272,13 @@ public struct ChatItem: Identifiable, Decodable, Hashable { public var timestampText: Text { meta.timestampText } - public var text: String { - switch (content.text, content.msgContent, file) { + public var text: String { text(isChannel: false) } + + public func text(isChannel: Bool) -> String { + switch (content.text(isChannel: isChannel), content.msgContent, file) { case let ("", .some(.voice(_, duration)), _): return "Voice message (\(durationText(duration)))" case let ("", _, .some(file)): return file.fileName - default: return content.text + default: return content.text(isChannel: isChannel) } } @@ -3265,6 +3348,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable { case .rcvCall: return false // notification is shown on .callInvitation instead case .rcvIntegrityError: return false case .rcvDecryptionError: return false + case .rcvMsgError: return false case .rcvGroupInvitation: return true case .sndGroupInvitation: return false case .rcvDirectEvent(rcvDirectEvent: let rcvDirectEvent): @@ -4022,6 +4106,7 @@ public enum CIContent: Decodable, ItemContent, Hashable { case rcvCall(status: CICallStatus, duration: Int) case rcvIntegrityError(msgError: MsgErrorType) case rcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt32) + case rcvMsgError(rcvMsgError: RcvMsgError) case rcvGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole) case sndGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole) case rcvDirectEvent(rcvDirectEvent: RcvDirectEvent) @@ -4047,42 +4132,43 @@ public enum CIContent: Decodable, ItemContent, Hashable { case chatBanner case invalidJSON(json: Data?) - public var text: String { - get { - switch self { - case let .sndMsgContent(mc): return mc.text - case let .rcvMsgContent(mc): return mc.text - case .sndDeleted: return NSLocalizedString("deleted", comment: "deleted chat item") - case .rcvDeleted: return NSLocalizedString("deleted", comment: "deleted chat item") - case let .sndCall(status, duration): return status.text(duration) - case let .rcvCall(status, duration): return status.text(duration) - case let .rcvIntegrityError(msgError): return msgError.text - case let .rcvDecryptionError(msgDecryptError, _): return msgDecryptError.text - case let .rcvGroupInvitation(groupInvitation, _): return groupInvitation.text - case let .sndGroupInvitation(groupInvitation, _): return groupInvitation.text - case let .rcvDirectEvent(rcvDirectEvent): return rcvDirectEvent.text - case let .rcvGroupEvent(rcvGroupEvent): return rcvGroupEvent.text - case let .sndGroupEvent(sndGroupEvent): return sndGroupEvent.text - case let .rcvConnEvent(rcvConnEvent): return rcvConnEvent.text - case let .sndConnEvent(sndConnEvent): return sndConnEvent.text - case let .rcvChatFeature(feature, enabled, param): return CIContent.featureText(feature, enabled.text, param) - case let .sndChatFeature(feature, enabled, param): return CIContent.featureText(feature, enabled.text, param) - case let .rcvChatPreference(feature, allowed, param): return CIContent.preferenceText(feature, allowed, param) - case let .sndChatPreference(feature, allowed, param): return CIContent.preferenceText(feature, allowed, param) - case let .rcvGroupFeature(feature, preference, param, role): return CIContent.featureText(feature, preference.enable.text, param, role) - case let .sndGroupFeature(feature, preference, param, role): return CIContent.featureText(feature, preference.enable.text, param, role) - case let .rcvChatFeatureRejected(feature): return String.localizedStringWithFormat("%@: received, prohibited", feature.text) - case let .rcvGroupFeatureRejected(groupFeature): return String.localizedStringWithFormat("%@: received, prohibited", groupFeature.text) - case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item") - case .rcvModerated: return NSLocalizedString("moderated", comment: "moderated chat item") - case .rcvBlocked: return NSLocalizedString("blocked by admin", comment: "blocked chat item") - case let .sndDirectE2EEInfo(e2eeInfo): return directE2EEInfoStr(e2eeInfo) - case let .rcvDirectE2EEInfo(e2eeInfo): return directE2EEInfoStr(e2eeInfo) - case .sndGroupE2EEInfo: return e2eeInfoNoPQStr - case .rcvGroupE2EEInfo: return e2eeInfoNoPQStr - case .chatBanner: return "" - case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item") - } + public var text: String { text(isChannel: false) } + + public func text(isChannel: Bool) -> String { + switch self { + case let .sndMsgContent(mc): return mc.text + case let .rcvMsgContent(mc): return mc.text + case .sndDeleted: return NSLocalizedString("deleted", comment: "deleted chat item") + case .rcvDeleted: return NSLocalizedString("deleted", comment: "deleted chat item") + case let .sndCall(status, duration): return status.text(duration) + case let .rcvCall(status, duration): return status.text(duration) + case let .rcvIntegrityError(msgError): return msgError.text + case let .rcvDecryptionError(msgDecryptError, _): return msgDecryptError.text + case let .rcvMsgError(rcvMsgError): return rcvMsgError.text + case let .rcvGroupInvitation(groupInvitation, _): return groupInvitation.text + case let .sndGroupInvitation(groupInvitation, _): return groupInvitation.text + case let .rcvDirectEvent(rcvDirectEvent): return rcvDirectEvent.text + case let .rcvGroupEvent(rcvGroupEvent): return rcvGroupEvent.text(isChannel: isChannel) + case let .sndGroupEvent(sndGroupEvent): return sndGroupEvent.text(isChannel: isChannel) + case let .rcvConnEvent(rcvConnEvent): return rcvConnEvent.text + case let .sndConnEvent(sndConnEvent): return sndConnEvent.text + case let .rcvChatFeature(feature, enabled, param): return CIContent.featureText(feature, enabled.text, param) + case let .sndChatFeature(feature, enabled, param): return CIContent.featureText(feature, enabled.text, param) + case let .rcvChatPreference(feature, allowed, param): return CIContent.preferenceText(feature, allowed, param) + case let .sndChatPreference(feature, allowed, param): return CIContent.preferenceText(feature, allowed, param) + case let .rcvGroupFeature(feature, preference, param, role): return CIContent.featureText(feature, preference.enable.text, param, role) + case let .sndGroupFeature(feature, preference, param, role): return CIContent.featureText(feature, preference.enable.text, param, role) + case let .rcvChatFeatureRejected(feature): return String.localizedStringWithFormat("%@: received, prohibited", feature.text) + case let .rcvGroupFeatureRejected(groupFeature): return String.localizedStringWithFormat("%@: received, prohibited", groupFeature.text) + case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item") + case .rcvModerated: return NSLocalizedString("moderated", comment: "moderated chat item") + case .rcvBlocked: return NSLocalizedString("blocked by admin", comment: "blocked chat item") + case let .sndDirectE2EEInfo(e2eeInfo): return directE2EEInfoStr(e2eeInfo) + case let .rcvDirectE2EEInfo(e2eeInfo): return directE2EEInfoStr(e2eeInfo) + case let .sndGroupE2EEInfo(e2eeInfo): return groupE2EEInfoStr(e2eeInfo) + case let .rcvGroupE2EEInfo(e2eeInfo): return groupE2EEInfoStr(e2eeInfo) + case .chatBanner: return "" + case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item") } } @@ -4092,6 +4178,12 @@ public enum CIContent: Decodable, ItemContent, Hashable { : e2eeInfoNoPQStr } + private func groupE2EEInfoStr(_ e2eeInfo: E2EEInfo) -> String { + e2eeInfo.public == true + ? NSLocalizedString("Messages in this channel are not end-to-end encrypted. Chat relays can see these messages.", comment: "E2EE info chat item") + : e2eeInfoNoPQStr + } + private var e2eeInfoNoPQStr: String { NSLocalizedString("This chat is protected by end-to-end encryption.", comment: "E2EE info chat item") } @@ -4150,6 +4242,7 @@ public enum CIContent: Decodable, ItemContent, Hashable { case .rcvCall: return true case .rcvIntegrityError: return true case .rcvDecryptionError: return true + case .rcvMsgError: return true case .rcvGroupInvitation: return true case .rcvModerated: return true case .rcvBlocked: return true @@ -4579,10 +4672,14 @@ public enum MsgContent: Equatable, Hashable { case voice(text: String, duration: Int) case file(String) case report(text: String, reason: ReportReason) - case chat(text: String, chatLink: MsgChatLink) + case chat(text: String, chatLink: MsgChatLink, ownerSig: LinkOwnerSig?) // TODO include original JSON, possibly using https://github.com/zoul/generic-json-swift case unknown(type: String, text: String) + public var chatLinkStr: String? { + if case let .chat(_, chatLink, _) = self { chatLink.connLinkStr } else { nil } + } + public var text: String { switch self { case let .text(text): return text @@ -4592,7 +4689,7 @@ public enum MsgContent: Equatable, Hashable { case let .voice(text, _): return text case let .file(text): return text case let .report(text, _): return text - case let .chat(text, _): return text + case let .chat(text, _, _): return text case let .unknown(_, text): return text } } @@ -4655,6 +4752,7 @@ public enum MsgContent: Equatable, Hashable { case duration case reason case chatLink + case ownerSig } public static func == (lhs: MsgContent, rhs: MsgContent) -> Bool { @@ -4666,7 +4764,7 @@ public enum MsgContent: Equatable, Hashable { case let (.voice(lt, ld), .voice(rt, rd)): return lt == rt && ld == rd case let (.file(lf), .file(rf)): return lf == rf case let (.report(lt, lr), .report(rt, rr)): return lt == rt && lr == rr - case let (.chat(lt, ll), .chat(rt, rl)): return lt == rt && ll == rl + case let (.chat(lt, ll, ls), .chat(rt, rl, rs)): return lt == rt && ll == rl && ls == rs case let (.unknown(lType, lt), .unknown(rType, rt)): return lType == rType && lt == rt default: return false } @@ -4709,7 +4807,8 @@ extension MsgContent: Decodable { case "chat": let text = try container.decode(String.self, forKey: CodingKeys.text) let chatLink = try container.decode(MsgChatLink.self, forKey: CodingKeys.chatLink) - self = .chat(text: text, chatLink: chatLink) + let ownerSig = try container.decodeIfPresent(LinkOwnerSig.self, forKey: CodingKeys.ownerSig) + self = .chat(text: text, chatLink: chatLink, ownerSig: ownerSig) default: let text = try? container.decode(String.self, forKey: CodingKeys.text) self = .unknown(type: type, text: text ?? "unknown message format") @@ -4751,10 +4850,11 @@ extension MsgContent: Encodable { try container.encode("report", forKey: .type) try container.encode(text, forKey: .text) try container.encode(reason, forKey: .reason) - case let .chat(text, chatLink): + case let .chat(text, chatLink, ownerSig): try container.encode("chat", forKey: .type) try container.encode(text, forKey: .text) try container.encode(chatLink, forKey: .chatLink) + try container.encodeIfPresent(ownerSig, forKey: .ownerSig) // TODO use original JSON and type case let .unknown(_, text): try container.encode("text", forKey: .type) @@ -4763,7 +4863,7 @@ extension MsgContent: Encodable { } } -public enum MsgContentTag: String, Decodable { +public enum MsgContentTag: Codable, Hashable { case text case link case image @@ -4771,12 +4871,195 @@ public enum MsgContentTag: String, Decodable { case voice case file case report + case chat + case unknown(type: String) + + public var rawValue: String { + switch self { + case .text: return "text" + case .link: return "link" + case .image: return "image" + case .video: return "video" + case .voice: return "voice" + case .file: return "file" + case .report: return "report" + case .chat: return "chat" + case let .unknown(type): return type + } + } + + public init(from decoder: Decoder) throws { + let s = try decoder.singleValueContainer().decode(String.self) + switch s { + case "text": self = .text + case "link": self = .link + case "image": self = .image + case "video": self = .video + case "voice": self = .voice + case "file": self = .file + case "report": self = .report + case "chat": self = .chat + case "liveText": self = .text + default: self = .unknown(type: s) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } } -public enum MsgChatLink: Codable, Equatable, Hashable { +public enum MsgChatLink: Equatable, Hashable { case contact(connLink: String, profile: Profile, business: Bool) case invitation(invLink: String, profile: Profile) case group(connLink: String, groupProfile: GroupProfile) + + public var isPublicGroup: Bool { + if case let .group(_, gp) = self { gp.publicGroup != nil } else { false } + } + + public var connLinkStr: String { + switch self { + case let .group(connLink, _): connLink + case let .contact(connLink, _, _): connLink + case let .invitation(invLink, _): invLink + } + } + + public var image: String? { + switch self { + case let .group(_, groupProfile): groupProfile.image + case let .contact(_, profile, _): profile.image + case let .invitation(_, profile): profile.image + } + } + + public var displayName: String { + switch self { + case let .group(_, groupProfile): groupProfile.displayName + case let .contact(_, profile, _): profile.displayName + case let .invitation(_, profile): profile.displayName + } + } + + public var iconName: String { + switch self { + case let .group(_, groupProfile): + groupProfile.isChannel ? "antenna.radiowaves.left.and.right.circle.fill" : "person.2.circle.fill" + case let .contact(_, _, business): + business ? "briefcase.circle.fill" : "person.crop.circle.fill" + case .invitation: + "person.crop.circle.fill" + } + } + + public var smallIconName: String { + switch self { + case let .group(_, groupProfile): + groupProfile.isChannel ? "antenna.radiowaves.left.and.right" : "person.2" + case let .contact(_, _, business): + business ? "briefcase" : "person" + case .invitation: + "person" + } + } + + public var fullName: String { + switch self { + case let .group(_, groupProfile): groupProfile.fullName + case let .contact(_, profile, _): profile.fullName + case let .invitation(_, profile): profile.fullName + } + } + + public var shortDescription: String? { + let s: String? = switch self { + case let .group(_, groupProfile): groupProfile.shortDescr + case let .contact(_, profile, _): profile.shortDescr + case let .invitation(_, profile): profile.shortDescr + } + if let d = s?.trimmingCharacters(in: .whitespacesAndNewlines), !d.isEmpty { return d } + return nil + } + + public func infoLine(signed: Bool) -> String { + var s: String = switch self { + case let .group(_, groupProfile): + groupProfile.isChannel + ? NSLocalizedString("Channel link", comment: "chat link info line") + : NSLocalizedString("Group link", comment: "chat link info line") + case let .contact(_, _, business): + business + ? NSLocalizedString("Business address", comment: "chat link info line") + : NSLocalizedString("Contact address", comment: "chat link info line") + case .invitation: + NSLocalizedString("One-time link", comment: "chat link info line") + } + if signed { + s += " " + ( + self.isPublicGroup + ? NSLocalizedString("(from owner)", comment: "chat link info line") + : NSLocalizedString("(signed)", comment: "chat link info line") + ) + } + return s + } +} + +extension MsgChatLink: Decodable { + private enum CodingKeys: String, CodingKey { + case type, connLink, invLink, profile, business, groupProfile + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + switch type { + case "contact": + let connLink = try container.decode(String.self, forKey: .connLink) + let profile = try container.decode(Profile.self, forKey: .profile) + let business = try container.decode(Bool.self, forKey: .business) + self = .contact(connLink: connLink, profile: profile, business: business) + case "invitation": + let invLink = try container.decode(String.self, forKey: .invLink) + let profile = try container.decode(Profile.self, forKey: .profile) + self = .invitation(invLink: invLink, profile: profile) + case "group": + let connLink = try container.decode(String.self, forKey: .connLink) + let groupProfile = try container.decode(GroupProfile.self, forKey: .groupProfile) + self = .group(connLink: connLink, groupProfile: groupProfile) + default: + throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown MsgChatLink type: \(type)") + } + } +} + +extension MsgChatLink: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .contact(connLink, profile, business): + try container.encode("contact", forKey: .type) + try container.encode(connLink, forKey: .connLink) + try container.encode(profile, forKey: .profile) + try container.encode(business, forKey: .business) + case let .invitation(invLink, profile): + try container.encode("invitation", forKey: .type) + try container.encode(invLink, forKey: .invLink) + try container.encode(profile, forKey: .profile) + case let .group(connLink, groupProfile): + try container.encode("group", forKey: .type) + try container.encode(connLink, forKey: .connLink) + try container.encode(groupProfile, forKey: .groupProfile) + } + } +} + +public struct LinkOwnerSig: Codable, Equatable, Hashable { + public var ownerId: String? + public var chatBinding: String + public var ownerSig: String } public struct FormattedText: Decodable, Hashable { @@ -5076,6 +5359,20 @@ public enum MsgErrorType: Decodable, Hashable { } } +public enum RcvMsgError: Decodable, Hashable { + case dropped(attempts: Int) + case parseError(parseError: String) + + var text: String { + switch self { + case let .dropped(attempts): + String.localizedStringWithFormat(NSLocalizedString("removed (%d attempts)", comment: "receive error chat item"), attempts) + case let .parseError(parseError): + String.localizedStringWithFormat(NSLocalizedString("error: %@", comment: "receive error chat item"), parseError) + } + } +} + public struct CIGroupInvitation: Decodable, Hashable { public var groupId: Int64 public var groupMemberId: Int64 @@ -5101,6 +5398,7 @@ public enum CIGroupInvitationStatus: String, Decodable, Hashable { public struct E2EEInfo: Decodable, Hashable { public var pqEnabled: Bool? + public var `public`: Bool? } public enum RcvDirectEvent: Decodable, Hashable { @@ -5153,7 +5451,9 @@ public enum RcvGroupEvent: Decodable, Hashable { case memberProfileUpdated(fromProfile: Profile, toProfile: Profile) case newMemberPendingReview - var text: String { + var text: String { text(isChannel: false) } + + func text(isChannel: Bool) -> String { switch self { case let .memberAdded(_, profile): return String.localizedStringWithFormat(NSLocalizedString("invited %@", comment: "rcv group event chat item"), profile.profileViewName) @@ -5175,8 +5475,12 @@ public enum RcvGroupEvent: Decodable, Hashable { case let .memberDeleted(_, profile): return String.localizedStringWithFormat(NSLocalizedString("removed %@", comment: "rcv group event chat item"), profile.profileViewName) case .userDeleted: return NSLocalizedString("removed you", comment: "rcv group event chat item") - 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 .groupDeleted: return isChannel + ? NSLocalizedString("deleted channel", comment: "rcv group event chat item") + : NSLocalizedString("deleted group", comment: "rcv group event chat item") + case .groupUpdated: return isChannel + ? NSLocalizedString("updated channel profile", comment: "rcv group event chat item") + : 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("requested connection", comment: "rcv group event chat item") case let .memberProfileUpdated(fromProfile, toProfile): return profileUpdatedText(fromProfile, toProfile) @@ -5208,7 +5512,9 @@ public enum SndGroupEvent: Decodable, Hashable { case memberAccepted(groupMemberId: Int64, profile: Profile) case userPendingReview - var text: String { + var text: String { text(isChannel: false) } + + func text(isChannel: Bool) -> String { switch self { case let .memberRole(_, profile, role): return String.localizedStringWithFormat(NSLocalizedString("you changed role of %@ to %@", comment: "snd group event chat item"), profile.profileViewName, role.text) @@ -5223,7 +5529,9 @@ public enum SndGroupEvent: Decodable, Hashable { case let .memberDeleted(_, profile): return String.localizedStringWithFormat(NSLocalizedString("you removed %@", comment: "snd group event chat item"), profile.profileViewName) case .userLeft: return NSLocalizedString("you left", comment: "snd group event chat item") - case .groupUpdated: return NSLocalizedString("group profile updated", comment: "snd group event chat item") + case .groupUpdated: return isChannel + ? NSLocalizedString("channel profile updated", comment: "snd group event chat item") + : NSLocalizedString("group profile updated", comment: "snd group event chat item") case .memberAccepted: return NSLocalizedString("you accepted this member", comment: "snd group event chat item") case .userPendingReview: return NSLocalizedString("Please wait for group moderators to review your request to join the group.", comment: "snd group event chat item") diff --git a/apps/ios/SimpleXChat/ChatUtils.swift b/apps/ios/SimpleXChat/ChatUtils.swift index 451ac8b4ef..788ac12bae 100644 --- a/apps/ios/SimpleXChat/ChatUtils.swift +++ b/apps/ios/SimpleXChat/ChatUtils.swift @@ -25,6 +25,7 @@ extension ChatLike { case .files: p.files.on(for: groupInfo.membership) case .simplexLinks: p.simplexLinks.on(for: groupInfo.membership) case .history: p.history.on + case .support: p.support.on case .reports: p.reports.on } } else { diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index 24dc58202a..a40e8eda99 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -74,7 +74,7 @@ public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ return createNotification( categoryIdentifier: ntfCategoryMessageReceived, title: title, - body: previewMode == .message ? hideSecrets(cItem) : NSLocalizedString("new message", comment: "notification"), + body: previewMode == .message ? hideSecrets(cItem, isChannel: cInfo.isChannel) : NSLocalizedString("new message", comment: "notification"), targetContentIdentifier: cInfo.id, userInfo: ["userId": user.userId], // userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id] @@ -197,7 +197,7 @@ public func createNotification( } // Spec: spec/services/notifications.md#hideSecrets -func hideSecrets(_ cItem: ChatItem) -> String { +func hideSecrets(_ cItem: ChatItem, isChannel: Bool = false) -> String { if let md = cItem.formattedText { var res = "" for ft in md { @@ -213,7 +213,7 @@ func hideSecrets(_ cItem: ChatItem) -> String { if case let .report(text, reason) = mc { return String.localizedStringWithFormat(NSLocalizedString("Report: %@", comment: "report in notification"), text.isEmpty ? reason.text : text) } else { - return cItem.text + return cItem.text(isChannel: isChannel) } } } diff --git a/apps/ios/SimpleXChat/Theme/Color.swift b/apps/ios/SimpleXChat/Theme/Color.swift index f307eaa5aa..86eefa4482 100644 --- a/apps/ios/SimpleXChat/Theme/Color.swift +++ b/apps/ios/SimpleXChat/Theme/Color.swift @@ -33,6 +33,55 @@ let HighOrLowlight = Color(139, 135, 134, a: 255) //let FileLight = Color(183, 190, 199, a: 255) //let FileDark = Color(101, 101, 106, a: 255) +// Create a Display P3 Color from oklch components. H in degrees +public func oklch(_ L: Double, _ C: Double, _ H: Double, alpha: Double = 1.0) -> Color { + let hRad = H * .pi / 180.0 + let cosH = cos(hRad) + let sinH = sin(hRad) + + func linearP3(C: Double) -> (Double, Double, Double) { + let a = C * cosH + let b = C * sinH + // oklab → LMS (Ottosson 2021) + let l_ = L + 0.3963377774 * a + 0.2158037573 * b + let m_ = L - 0.1055613458 * a - 0.0638541728 * b + let s_ = L - 0.0894841775 * a - 1.2914855480 * b + let l = l_ * l_ * l_ + let m = m_ * m_ * m_ + let s = s_ * s_ * s_ + // LMS → linear Display P3 (direct, no sRGB clamping) + return ( + 3.1281105148 * l - 2.2570749853 * m + 0.1293047593 * s, + -1.0911282009 * l + 2.4132668169 * m - 0.3221681599 * s, + -0.0260136845 * l - 0.5080276339 * m + 1.5333166364 * s + ) + } + + func inGamut(_ r: Double, _ g: Double, _ b: Double) -> Bool { + r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1 + } + + // linear P3 → gamma-encoded P3 (same transfer function as sRGB) + func gammaEncode(_ x: Double) -> Double { + x >= 0.0031308 + ? 1.055 * pow(min(x, 1.0), 1.0 / 2.4) - 0.055 + : 12.92 * max(x, 0) + } + + var (r, g, b) = linearP3(C: C) + if !inGamut(r, g, b) { + var lo = 0.0, hi = C + while hi - lo > 1e-5 { + let mid = (lo + hi) / 2 + let (mr, mg, mb) = linearP3(C: mid) + if inGamut(mr, mg, mb) { lo = mid; r = mr; g = mg; b = mb } + else { hi = mid } + } + } + + return Color(.displayP3, red: gammaEncode(r), green: gammaEncode(g), blue: gammaEncode(b), opacity: alpha) +} + extension Color { public init(_ argb: Int64) { let a = Double((argb & 0xFF000000) >> 24) / 255.0 diff --git a/apps/ios/SimpleXChat/exported_symbols.txt b/apps/ios/SimpleXChat/exported_symbols.txt new file mode 100644 index 0000000000..52c3bf83e9 --- /dev/null +++ b/apps/ios/SimpleXChat/exported_symbols.txt @@ -0,0 +1,11 @@ +# Swift mangled symbols (Swift 5+ ABI stable prefix) +_$s* + +# ObjC class/metaclass symbols (for NSObject subclasses) +_OBJC_CLASS_$_* +_OBJC_METACLASS_$_* + +# C API (SimpleX.h bridging header) +_chat_* +_haskell_init* +_hs_init* diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index fb8529fb88..17e11d1020 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -25,15 +25,9 @@ /* No comment provided by engineer. */ "(this device v%@)" = "(това устройство v%@)"; -/* 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. */ "**Create 1-time link**: to create and share a new invitation link." = "**Добави контакт**: за създаване на нов линк."; @@ -389,9 +383,6 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "Активни връзки"; -/* 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 friends" = "Добави приятели"; @@ -635,9 +626,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Отговор на повикване"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "Протокол и код с отворен код – всеки може да оперира собствени сървъри."; - /* No comment provided by engineer. */ "App build: %@" = "Компилация на приложението: %@"; @@ -873,7 +861,7 @@ marked deleted chat item preview text */ /* 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. */ +/* chat link info line */ "Business address" = "Бизнес адрес"; /* No comment provided by engineer. */ @@ -888,9 +876,6 @@ marked deleted chat item preview text */ /* 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. */ -"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "С използването на SimpleX Chat вие се съгласявате със:\n- изпращане само на легално съдържание в публични групи.\n- уважение към другите потребители – без спам."; - /* No comment provided by engineer. */ "Call already ended!" = "Разговорът вече приключи!"; @@ -1068,7 +1053,8 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "Чатът ще бъде изтрит за вас - това не може да бъде отменено!"; -/* chat toolbar */ +/* chat feature +chat toolbar */ "Chat with admins" = "Чат с администраторите"; /* No comment provided by engineer. */ @@ -1182,9 +1168,6 @@ set passcode view */ /* No comment provided by engineer. */ "Configure ICE servers" = "Конфигурирай ICE сървъри"; -/* No comment provided by engineer. */ -"Configure server operators" = "Конфигуриране на сървърни оператори"; - /* No comment provided by engineer. */ "Confirm" = "Потвърди"; @@ -1218,7 +1201,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Потвърдено"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Свързване"; /* No comment provided by engineer. */ @@ -1317,7 +1301,7 @@ set passcode view */ /* alert title */ "Connection error" = "Грешка при свързване"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "Грешка при свързване (AUTH)"; /* chat list item title (it should not be shown */ @@ -1383,18 +1367,18 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "Продължи"; +/* No comment provided by engineer. */ +"Contribute" = "Допринеси"; + /* No comment provided by engineer. */ "Copy" = "Копирай"; /* No comment provided by engineer. */ "Core version: v%@" = "Версия на ядрото: v%@"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Поправи име на %@?"; -/* No comment provided by engineer. */ -"Create" = "Създаване"; - /* No comment provided by engineer. */ "Create 1-time link" = "Създаване на еднократна препратка"; @@ -1521,9 +1505,6 @@ set passcode view */ /* time unit */ "days" = "дни"; -/* No comment provided by engineer. */ -"Decentralized" = "Децентрализиран"; - /* message decrypt error item */ "Decryption error" = "Грешка при декриптиране"; @@ -1801,7 +1782,7 @@ chat item action */ /* No comment provided by engineer. */ "Edit group profile" = "Редактирай групов профил"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "Активирай"; /* No comment provided by engineer. */ @@ -1825,9 +1806,6 @@ chat item action */ /* No comment provided by engineer. */ "Enable lock" = "Активирай заключване"; -/* No comment provided by engineer. */ -"Enable notifications" = "Активирай известията"; - /* No comment provided by engineer. */ "Enable periodic notifications?" = "Активирай периодични известия?"; @@ -1963,7 +1941,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "грешка"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "Грешка при свързване със сървъра"; /* No comment provided by engineer. */ @@ -2211,7 +2189,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Намирайте чатове по-бързо"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен"; /* No comment provided by engineer. */ @@ -2304,7 +2283,7 @@ snd error text */ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "Груповата покана вече е невалидна, премахната е от подателя."; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "Групов линк"; /* No comment provided by engineer. */ @@ -2412,9 +2391,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "Веднага"; -/* No comment provided by engineer. */ -"Immune to spam" = "Защитен от спам и злоупотреби"; - /* No comment provided by engineer. */ "Import" = "Импортиране"; @@ -2500,7 +2476,7 @@ snd error text */ "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)"; +"Install SimpleX Chat for terminal" = "Инсталирайте SimpleX Chat за терминал"; /* No comment provided by engineer. */ "Instant" = "Мигновено"; @@ -2517,7 +2493,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "невалидни данни за чат"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "Невалиден линк за връзка"; /* invalid chat item */ @@ -2532,7 +2508,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Невалидно потвърждение за мигриране"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Невалидно име!"; /* No comment provided by engineer. */ @@ -2823,9 +2799,6 @@ snd error text */ /* No comment provided by engineer. */ "Migrate device" = "Мигрирай устройството"; -/* No comment provided by engineer. */ -"Migrate from another device" = "Мигриране от друго устройство"; - /* No comment provided by engineer. */ "Migrate here" = "Мигрирай тук"; @@ -3000,9 +2973,6 @@ snd error text */ /* copied message info in history */ "no text" = "няма текст"; -/* No comment provided by engineer. */ -"No user identifiers." = "Първата платформа без никакви потребителски идентификатори – поверителна по дизайн."; - /* No comment provided by engineer. */ "Not compatible!" = "Несъвместим!"; @@ -3038,7 +3008,7 @@ alert button new chat action */ "Ok" = "Ок"; -/* No comment provided by engineer. */ +/* alert button */ "OK" = "ОК"; /* No comment provided by engineer. */ @@ -3101,7 +3071,8 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Само вашият контакт може да изпраща гласови съобщения."; -/* alert action */ +/* alert action +alert button */ "Open" = "Отвори"; /* new chat action */ @@ -3248,9 +3219,6 @@ new chat action */ /* No comment provided by engineer. */ "Privacy & security" = "Поверителност и сигурност"; -/* No comment provided by engineer. */ -"Privacy redefined" = "Поверителността преосмислена"; - /* No comment provided by engineer. */ "Private filenames" = "Поверителни имена на файлове"; @@ -3269,9 +3237,6 @@ new chat action */ /* No comment provided by engineer. */ "Profile password" = "Профилна парола"; -/* alert message */ -"Profile update will be sent to your contacts." = "Актуализацията на профила ще бъде изпратена до вашите контакти."; - /* No comment provided by engineer. */ "Prohibit audio/video calls." = "Забрани аудио/видео разговорите."; @@ -3336,16 +3301,10 @@ new chat action */ "Read more" = "Прочетете още"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in our GitHub repository." = "Прочетете повече в нашето GitHub хранилище."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; - -/* 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)."; +"Read more in User Guide." = "Прочетете повече в Ръководство за потребителя."; /* No comment provided by engineer. */ "Receipts are disabled" = "Потвърждениeто за доставка е деактивирано"; @@ -3780,18 +3739,12 @@ chat item action */ /* No comment provided by engineer. */ "Share address" = "Сподели адрес"; -/* alert title */ -"Share address with contacts?" = "Сподели адреса с контактите?"; - /* No comment provided by engineer. */ "Share link" = "Сподели линк"; /* No comment provided by engineer. */ "Share this 1-time invite link" = "Сподели този еднократен линк за връзка"; -/* No comment provided by engineer. */ -"Share with contacts" = "Сподели с контактите"; - /* No comment provided by engineer. */ "Show calls in phone history" = "Показване на обажданията в хронологията на телефона"; @@ -3879,6 +3832,9 @@ chat item action */ /* chat item text */ "standard end-to-end encryption" = "стандартно криптиране от край до край"; +/* No comment provided by engineer. */ +"Star on GitHub" = "Звезда в GitHub"; + /* No comment provided by engineer. */ "Start chat" = "Започни чат"; @@ -3975,7 +3931,8 @@ chat item action */ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Тестът е неуспешен на стъпка %@."; /* No comment provided by engineer. */ @@ -4017,9 +3974,6 @@ chat item action */ /* 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 future of messaging" = "Ново поколение поверителни съобщения"; - /* No comment provided by engineer. */ "The hash of the previous message is different." = "Хешът на предишното съобщение е различен."; @@ -4248,9 +4202,6 @@ chat item action */ /* No comment provided by engineer. */ "Use .onion hosts" = "Използвай .onion хостове"; -/* No comment provided by engineer. */ -"Use chat" = "Използвай чата"; - /* new chat action */ "Use current profile" = "Използвай текущия профил"; @@ -4554,9 +4505,6 @@ chat item action */ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Не можахте да бъдете потвърдени; Моля, опитайте отново."; -/* No comment provided by engineer. */ -"You decide who can connect." = "Хората могат да се свържат с вас само чрез ликовете, които споделяте."; - /* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Вече сте направили заявката за връзка!\nИзпрати отново заявката за свързване?"; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 9e1fe7139c..f7e90e0c88 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -25,15 +25,9 @@ /* No comment provided by engineer. */ "(this device v%@)" = "(toto zařízení v%@)"; -/* No comment provided by engineer. */ -"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Přispějte](https://github.com/simplex-chat/simplex-chat#contribute)"; - /* No comment provided by engineer. */ "[Send us email](mailto:chat@simplex.chat)" = "[Pošlete nám e-mail](mailto:chat@simplex.chat)"; -/* No comment provided by engineer. */ -"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Hvězda na GitHubu](https://github.com/simplex-chat/simplex-chat)"; - /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Vytvořit jednorázový odkaz**: pro vytvoření a sdílení nové pozvánky."; @@ -196,6 +190,9 @@ /* No comment provided by engineer. */ "%lld file(s) with total size of %@" = "%lld soubor(y) s celkovou velikostí %@"; +/* No comment provided by engineer. */ +"%lld group events" = "%lld událostí skupiny"; + /* No comment provided by engineer. */ "%lld members" = "%lld členové"; @@ -305,7 +302,7 @@ time interval */ "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**."; +"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ý profil chatu, který máte v aplikaci**."; /* 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." = "**pro každý kontakt a člena skupiny** bude použito samostatné připojení TCP.\n**Upozornění**: Pokud máte mnoho připojení, spotřeba baterie a provozu může být podstatně vyšší a některá připojení mohou selhat."; @@ -371,9 +368,6 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "Aktivní spojení"; -/* 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." = "Přidejte adresu do svého profilu, aby ji vaše kontakty mohly sdílet s dalšími lidmi. Aktualizace profilu bude zaslána vašim kontaktům."; - /* No comment provided by engineer. */ "Add friends" = "Přidat přátele"; @@ -453,7 +447,7 @@ swipe action */ "All chats and messages will be deleted - this cannot be undone!" = "Všechny chaty a zprávy budou smazány – tuto akci nelze vrátit zpět!"; /* alert message */ -"All chats will be removed from the list %@, and the list deleted." = "Všechny chaty budou odstraněny ze seznamu %@ a seznam bude odstraněn."; +"All chats will be removed from the list %@, and the list deleted." = "Všechny chaty budou odstraněny ze seznamu %@ a seznam bude smazán."; /* No comment provided by engineer. */ "All data is erased when it is entered." = "Všechna data se při zadání vymažou."; @@ -570,7 +564,7 @@ swipe action */ "Always use relay" = "Spojení přes relé"; /* No comment provided by engineer. */ -"An empty chat profile with the provided name is created, and the app opens as usual." = "Vytvořit prázdný chat profil se zadaným názvem a otevřít aplikaci jako obvykle."; +"An empty chat profile with the provided name is created, and the app opens as usual." = "Vytvořit prázdný profil chatu se zadaným názvem a otevřít aplikaci jako obvykle."; /* report reason */ "Another reason" = "Jiný důvod"; @@ -578,9 +572,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Přijmout hovor"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "Servery může provozovat kdokoli."; - /* No comment provided by engineer. */ "App build: %@" = "Sestavení aplikace: %@"; @@ -680,6 +671,12 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "Špatné ID zprávy"; +/* No comment provided by engineer. */ +"Be free in your network." = "Buďte svobodní ve své síti."; + +/* No comment provided by engineer. */ +"Because we destroyed the power to know who you are. So that your power can never be taken." = "Protože jsme zničili sílu vědět, kdo jste. Aby vám vaši moc nikdo nemohl vzít."; + /* No comment provided by engineer. */ "Better calls" = "Lepší volání"; @@ -743,11 +740,11 @@ swipe action */ /* 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)!" = "Bulharský, finský, thajský a ukrajinský - díky uživatelům a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; -/* No comment provided by engineer. */ +/* chat link info line */ "Business address" = "Obchodní adresa"; /* 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)." = "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)."; +"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Podle profilu chatu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; /* No comment provided by engineer. */ "Call already ended!" = "Hovor již skončil!"; @@ -821,7 +818,7 @@ new chat action */ "Change automatic message deletion?" = "Změnit automatické mazání zpráv?"; /* authentication reason */ -"Change chat profiles" = "Změnit chat profily"; +"Change chat profiles" = "Změnit profily chatu"; /* No comment provided by engineer. */ "Change database passphrase?" = "Změnit přístupovou frázi databáze?"; @@ -959,7 +956,8 @@ set passcode view */ /* No comment provided by engineer. */ "Confirm upload" = "Potvrdit nahrání"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Připojit"; /* No comment provided by engineer. */ @@ -1013,7 +1011,7 @@ set passcode view */ /* alert title */ "Connection error" = "Chyba připojení"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "Chyba spojení (AUTH)"; /* chat list item title (it should not be shown */ @@ -1061,15 +1059,15 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "Pokračovat"; +/* No comment provided by engineer. */ +"Contribute" = "Přispějte"; + /* No comment provided by engineer. */ "Copy" = "Kopírovat"; /* No comment provided by engineer. */ "Core version: v%@" = "Verze jádra: v%@"; -/* No comment provided by engineer. */ -"Create" = "Vytvořit"; - /* server test step */ "Create file" = "Vytvořit soubor"; @@ -1175,9 +1173,6 @@ set passcode view */ /* time unit */ "days" = "dní"; -/* No comment provided by engineer. */ -"Decentralized" = "Decentralizované"; - /* message decrypt error item */ "Decryption error" = "Chyba dešifrování"; @@ -1208,10 +1203,10 @@ swipe action */ "Delete all files" = "Odstranit všechny soubory"; /* No comment provided by engineer. */ -"Delete chat profile" = "Smazat chat profil"; +"Delete chat profile" = "Smazat profil chatu"; /* No comment provided by engineer. */ -"Delete chat profile?" = "Smazat chat profil?"; +"Delete chat profile?" = "Smazat profil chatu?"; /* No comment provided by engineer. */ "Delete connection" = "Smazat připojení"; @@ -1412,7 +1407,7 @@ alert button */ /* No comment provided by engineer. */ "Edit group profile" = "Upravit profil skupiny"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "Zapnout"; /* No comment provided by engineer. */ @@ -1430,9 +1425,6 @@ alert button */ /* No comment provided by engineer. */ "Enable lock" = "Povolit zámek"; -/* No comment provided by engineer. */ -"Enable notifications" = "Povolit upozornění"; - /* No comment provided by engineer. */ "Enable periodic notifications?" = "Povolit pravidelná oznámení?"; @@ -1544,7 +1536,7 @@ alert button */ /* No comment provided by engineer. */ "error" = "chyba"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "Chyba"; /* No comment provided by engineer. */ @@ -1753,7 +1745,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Najděte chaty rychleji"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Otisk certifikátu v adrese serveru neodpovídá."; /* No comment provided by engineer. */ @@ -1819,7 +1812,7 @@ snd error text */ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "Skupinová pozvánka již není platná, byla odstraněna odesílatelem."; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "Odkaz na skupinu"; /* No comment provided by engineer. */ @@ -1859,7 +1852,7 @@ snd error text */ "Hidden" = "Skryté"; /* No comment provided by engineer. */ -"Hidden chat profiles" = "Skryté chat profily"; +"Hidden chat profiles" = "Skryté profily chatu"; /* No comment provided by engineer. */ "Hidden profile password" = "Hesla skrytých profilů"; @@ -1921,9 +1914,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "Ihned"; -/* No comment provided by engineer. */ -"Immune to spam" = "Odolná vůči spamu a zneužití"; - /* No comment provided by engineer. */ "Import" = "Import"; @@ -1988,7 +1978,7 @@ snd error text */ "Initial role" = "Počáteční role"; /* No comment provided by engineer. */ -"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Nainstalujte [SimpleX Chat pro terminál](https://github.com/simplex-chat/simplex-chat)"; +"Install SimpleX Chat for terminal" = "Nainstalujte SimpleX Chat pro terminál"; /* No comment provided by engineer. */ "Instant" = "Okamžitě"; @@ -2005,7 +1995,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "neplatná chat data"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "Neplatný odkaz na spojení"; /* invalid chat item */ @@ -2291,7 +2281,7 @@ snd error text */ "Most likely this connection is deleted." = "Pravděpodobně je toto spojení smazáno."; /* No comment provided by engineer. */ -"Multiple chat profiles" = "Více chatovacích profilů"; +"Multiple chat profiles" = "Více profilů chatu"; /* notification label action */ "Mute" = "Ztlumit"; @@ -2390,7 +2380,10 @@ snd error text */ "no text" = "žádný text"; /* No comment provided by engineer. */ -"No user identifiers." = "Bez uživatelských identifikátorů."; +"Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "Nikdo nesledoval vaše konverzace. Nikdo nevytvořil mapu, kde jste byli. Soukromí nikdy nebylo funkcí - byl to způsob života."; + +/* No comment provided by engineer. */ +"Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "Nejde o to mít lepší zámek na dveřích někoho jiného. Ani o to mít nájemce, který respektuje vaše soukromí, ale vede evidenci všech vašich návštěvníků. Nejste host. Jste doma. Ani král k vám nemůže vstoupit - jste suverén."; /* No comment provided by engineer. */ "Notifications" = "Oznámení"; @@ -2484,7 +2477,8 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Hlasové zprávy může odesílat pouze váš kontakt."; -/* alert action */ +/* alert action +alert button */ "Open" = "Otevřít"; /* new chat action */ @@ -2586,9 +2580,6 @@ new chat action */ /* No comment provided by engineer. */ "Privacy & security" = "Ochrana osobních údajů a zabezpečení"; -/* No comment provided by engineer. */ -"Privacy redefined" = "Nové vymezení soukromí"; - /* No comment provided by engineer. */ "Private filenames" = "Soukromé názvy souborů"; @@ -2601,9 +2592,6 @@ new chat action */ /* No comment provided by engineer. */ "Profile password" = "Heslo profilu"; -/* alert message */ -"Profile update will be sent to your contacts." = "Aktualizace profilu bude zaslána vašim kontaktům."; - /* No comment provided by engineer. */ "Prohibit audio/video calls." = "Zákaz audio/video hovorů."; @@ -2632,7 +2620,7 @@ new chat action */ "Protect app screen" = "Ochrana obrazovky aplikace"; /* No comment provided by engineer. */ -"Protect your chat profiles with a password!" = "Chraňte své chat profily heslem!"; +"Protect your chat profiles with a password!" = "Chraňte své profily chatu pomocí hesla!"; /* No comment provided by engineer. */ "Protocol timeout" = "Časový limit protokolu"; @@ -2656,13 +2644,10 @@ new chat action */ "Read more" = "Přečíst více"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Další informace naleznete v [Uživatelské příručce](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; +"Read more in our GitHub repository." = "Přečtěte si více v našem GitHub repozitáři."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Přečtěte si více v [Uživatelské příručce](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)." = "Přečtěte si více v našem [GitHub repozitáři](https://github.com/simplex-chat/simplex-chat#readme)."; +"Read more in User Guide." = "Více informací v průvodci uživatele."; /* No comment provided by engineer. */ "Receipts are disabled" = "Informace o dodání jsou zakázány"; @@ -2775,7 +2760,7 @@ swipe action */ "Reset to defaults" = "Obnovení výchozího nastavení"; /* No comment provided by engineer. */ -"Restart the app to create a new chat profile" = "Restartujte aplikaci pro vytvoření nového chat profilu"; +"Restart the app to create a new chat profile" = "Restartujte aplikaci pro vytvoření nového profilu chatu"; /* No comment provided by engineer. */ "Restart the app to use imported chat database" = "Restartujte aplikaci pro použití importované databáze chatu"; @@ -2944,7 +2929,7 @@ chat item action */ "Sender may have deleted the connection request." = "Odesílatel možná smazal požadavek připojení."; /* No comment provided by engineer. */ -"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Odesílání potvrzení o doručení bude povoleno pro všechny kontakty ve všech viditelných chat profilech."; +"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Odesílání potvrzení o doručení bude povoleno pro všechny kontakty ve všech viditelných profilech chatu."; /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts." = "Odesílání potvrzení o doručení bude povoleno pro všechny kontakty."; @@ -3028,15 +3013,9 @@ chat item action */ /* No comment provided by engineer. */ "Share address" = "Sdílet adresu"; -/* alert title */ -"Share address with contacts?" = "Sdílet adresu s kontakty?"; - /* No comment provided by engineer. */ "Share link" = "Sdílet odkaz"; -/* No comment provided by engineer. */ -"Share with contacts" = "Sdílet s kontakty"; - /* No comment provided by engineer. */ "Show calls in phone history" = "Ukaž hovory v historii telefonu"; @@ -3106,6 +3085,9 @@ chat item action */ /* notification title */ "Somebody" = "Někdo"; +/* No comment provided by engineer. */ +"Star on GitHub" = "Hvězda na GitHubu"; + /* No comment provided by engineer. */ "Start chat" = "Začít chat"; @@ -3184,7 +3166,8 @@ chat item action */ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Test selhal v kroku %@."; /* No comment provided by engineer. */ @@ -3223,9 +3206,6 @@ chat item action */ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Šifrování funguje a nové povolení šifrování není vyžadováno. To může vyvolat chybu v připojení!"; -/* No comment provided by engineer. */ -"The future of messaging" = "Nová generace soukromých zpráv"; - /* No comment provided by engineer. */ "The hash of the previous message is different." = "Hash předchozí zprávy se liší."; @@ -3241,6 +3221,9 @@ chat item action */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Stará databáze nebyla během přenášení odstraněna, lze ji smazat."; +/* No comment provided by engineer. */ +"The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it." = "Nejstarší lidská svoboda - mluvit s druhým člověkem, aniž by byl sledován - postavena na infrastruktuře, která ji nemůže zradit."; + /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Druhé zaškrtnutí jsme přehlédli! ✅"; @@ -3248,7 +3231,13 @@ chat item action */ "The sender will NOT be notified" = "Odesílatel NEBUDE informován"; /* No comment provided by engineer. */ -"The servers for new connections of your current chat profile **%@**." = "Servery pro nová připojení vašeho aktuálního chat profilu **%@**."; +"The servers for new connections of your current chat profile **%@**." = "Servery pro nová připojení vašeho aktuálního profilu chatu **%@**."; + +/* No comment provided by engineer. */ +"Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible." = "Pak jsme se přesunuli na internet a každá platforma chtěla o vás něco vědět - vaše jméno, vaše číslo, vaše přátele. Smířili jsme se s tím, že cenou za komunikaci s ostatními je dát někomu vědět, s kým mluvíme. Každá generace, lidská i technická, to tak měla - telefon, e-mail, komunikátory, sociální sítě. Zdálo se, že je to jediný možný způsob."; + +/* No comment provided by engineer. */ +"There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected." = "Existuje i jiný způsob. Síť bez telefonních čísel. Bez uživatelských jmen. Bez účtů. Bez jakékoli uživatelské identity. Síť, která spojuje lidi a přenáší šifrované zprávy, aniž by bylo známo, kdo je připojen."; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Toto nastavení je pro váš aktuální profil **%@**."; @@ -3275,7 +3264,7 @@ chat item action */ "This group no longer exists." = "Tato skupina již neexistuje."; /* No comment provided by engineer. */ -"This setting applies to messages in your current chat profile **%@**." = "Toto nastavení platí pro zprávy ve vašem aktuálním chat profilu **%@**."; +"This setting applies to messages in your current chat profile **%@**." = "Toto nastavení platí pro zprávy ve vašem aktuálním profilu chatu **%@**."; /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Chcete-li položit jakékoli dotazy a dostávat aktuality:"; @@ -3299,7 +3288,7 @@ chat item action */ "To record voice message please grant permission to use Microphone." = "Chcete-li nahrávat hlasové zprávy, udělte povolení k použití mikrofonu."; /* No comment provided by engineer. */ -"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce **Chat profily**."; +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce **Profily chatu**."; /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Pro podporu doručování okamžitých upozornění musí být přenesena chat databáze."; @@ -3332,7 +3321,7 @@ chat item action */ "Unhide" = "Odkrýt"; /* No comment provided by engineer. */ -"Unhide chat profile" = "Odkrýt chat profil"; +"Unhide chat profile" = "Odkrýt profil chatu"; /* No comment provided by engineer. */ "Unhide profile" = "Odkrýt profil"; @@ -3394,9 +3383,6 @@ chat item action */ /* No comment provided by engineer. */ "Use .onion hosts" = "Použít hostitele .onion"; -/* No comment provided by engineer. */ -"Use chat" = "Použijte chat"; - /* new chat action */ "Use current profile" = "Použít aktuální profil"; @@ -3569,7 +3555,7 @@ chat item action */ "You can set lock screen notification preview via settings." = "Náhled oznámení na zamykací obrazovce můžete změnit v nastavení."; /* 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." = "Můžete sdílet odkaz nebo QR kód - ke skupině se bude moci připojit kdokoli. O členy skupiny nepřijdete, pokud ji později odstraníte."; +"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." = "Můžete sdílet odkaz nebo QR kód - ke skupině se bude moci připojit kdokoli. O členy skupiny nepřijdete, pokud odkaz později smažete."; /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Tuto adresu můžete sdílet s vašimi kontakty, abyse se mohli spojit s **%@**."; @@ -3601,9 +3587,6 @@ chat item action */ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Nemohli jste být ověřeni; Zkuste to prosím znovu."; -/* No comment provided by engineer. */ -"You decide who can connect." = "Lidé se s vámi mohou spojit pouze prostřednictvím odkazu, který sdílíte."; - /* No comment provided by engineer. */ "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í."; @@ -3640,6 +3623,9 @@ chat item action */ /* chat list item description */ "you shared one-time link incognito" = "sdíleli jste jednorázový odkaz inkognito"; +/* No comment provided by engineer. */ +"You were born without an account" = "Narodili jste se bez účtu."; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Ke skupině budete připojeni, až bude zařízení hostitele skupiny online, vyčkejte prosím nebo se podívejte později!"; @@ -3680,7 +3666,7 @@ chat item action */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Vaše chat databáze není šifrována – nastavte přístupovou frázi pro její šifrování."; /* No comment provided by engineer. */ -"Your chat profiles" = "Vaše chat profily"; +"Your chat profiles" = "Vaše profily chatu"; /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (%@)."; @@ -3691,6 +3677,9 @@ chat item action */ /* No comment provided by engineer. */ "Your contacts will remain connected." = "Vaše kontakty zůstanou připojeny."; +/* No comment provided by engineer. */ +"Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public." = "Vaše konverzace patří vám, jako tomu bylo vždy před internetem. Síť není místo, které navštěvujete. Je to místo, které vytváříte a vlastníte. A nikdo vám ho nemůže vzít, ať už je soukromé, nebo veřejné."; + /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Vaše aktuální chat databáze bude ODSTRANĚNA a NAHRAZENA importovanou."; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index e3979abc37..d5978b48dc 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -10,6 +10,9 @@ /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- stabilere Zustellung von Nachrichten.\n- ein bisschen verbesserte Gruppen.\n- und mehr!"; +/* No comment provided by engineer. */ +"- opt-in to send link previews.\n- prevent hyperlink phishing.\n- remove link tracking." = "- Opt‑in zum Senden von Linkvorschauen.\n- Hyperlink‑Phishing verhindern.\n- Link‑Tracking entfernen."; + /* No comment provided by engineer. */ "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- Optionale Benachrichtigung von gelöschten Kontakten.\n- Profilnamen mit Leerzeichen.\n- Und mehr!"; @@ -19,21 +22,21 @@ /* No comment provided by engineer. */ "!1 colored!" = "!1 farbig!"; +/* chat link info line */ +"(from owner)" = "(vom Eigentümer)"; + /* No comment provided by engineer. */ "(new)" = "(Neu)"; +/* chat link info line */ +"(signed)" = "(signiert)"; + /* No comment provided by engineer. */ "(this device v%@)" = "(Dieses Gerät hat v%@)"; -/* No comment provided by engineer. */ -"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Unterstützen Sie uns](https://github.com/simplex-chat/simplex-chat#contribute)"; - /* No comment provided by engineer. */ "[Send us email](mailto:chat@simplex.chat)" = "[Senden Sie uns eine E-Mail](mailto:chat@simplex.chat)"; -/* No comment provided by engineer. */ -"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Stern auf GitHub vergeben](https://github.com/simplex-chat/simplex-chat)"; - /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Kontakt hinzufügen**: Um einen neuen Einladungslink zu erstellen."; @@ -64,6 +67,9 @@ /* No comment provided by engineer. */ "**Scan / Paste link**: to connect via a link you received." = "**Link scannen / einfügen**: Um eine Verbindung über den Link herzustellen, den Sie erhalten haben."; +/* No comment provided by engineer. */ +"**Test relay** to retrieve its name." = "**Relais testen** um seinen Namen abzurufen."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Warnung**: Sofortige Push-Benachrichtigungen erfordern die Eingabe eines Passworts, welches in Ihrem Schlüsselbund gespeichert ist."; @@ -175,6 +181,18 @@ /* time interval */ "%d months" = "%d Monate"; +/* channel relay bar +channel subscriber relay bar */ +"%d relays failed" = "%d Relais fehlgeschlagen"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays not active" = "%d Relais nicht aktiv"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays removed" = "%d Relais entfernt"; + /* time interval */ "%d sec" = "%d s"; @@ -184,15 +202,50 @@ /* integrity error chat item */ "%d skipped message(s)" = "%d übersprungene Nachricht(en)"; +/* channel subscriber count */ +"%d subscriber" = "%d Abonnent"; + +/* channel subscriber count */ +"%d subscribers" = "%d Abonnenten"; + /* time interval */ "%d weeks" = "%d Wochen"; +/* channel creation progress +channel relay bar progress */ +"%d/%d relays active" = "%1$d/%2$d Relais aktiv"; + +/* channel relay bar */ +"%d/%d relays active, %d errors" = "%1$d/%2$d Relais aktiv, %3$d Fehler"; + +/* channel creation progress with errors +channel relay bar */ +"%d/%d relays active, %d failed" = "%1$d/%2$d Relais aktiv, %3$d fehlgeschlagen"; + +/* channel relay bar */ +"%d/%d relays active, %d removed" = "%1$d/%2$d Relais aktiv, %3$d entfernt"; + +/* channel subscriber relay bar progress */ +"%d/%d relays connected" = "%1$d/%2$d Relais verbunden"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d errors" = "%1$d/%2$d Relais verbunden, %3$d Fehler"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d failed" = "%1$d/%2$d Relais verbunden, %3$d fehlgeschlagen"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d removed" = "%1$d/%2$d Relais verbunden, %3$d entfernt"; + /* No comment provided by engineer. */ "%lld" = "%lld"; /* No comment provided by engineer. */ "%lld %@" = "%lld %@"; +/* No comment provided by engineer. */ +"%lld channel events" = "%lld Kanalereignisse"; + /* No comment provided by engineer. */ "%lld contact(s) selected" = "%lld Kontakt(e) ausgewählt"; @@ -262,6 +315,9 @@ /* No comment provided by engineer. */ "~strike~" = "\\~durchstreichen~"; +/* owner verification */ +"⚠️ Signature verification failed: %@." = "⚠️ Signaturüberprüfung fehlgeschlagen: %@."; + /* time to disappear */ "0 sec" = "0 sek"; @@ -307,6 +363,9 @@ time interval */ /* No comment provided by engineer. */ "A few more things" = "Ein paar weitere Dinge"; +/* No comment provided by engineer. */ +"A link for one person to connect" = "Verbindungs-Link für eine Person"; + /* notification title */ "A new contact" = "Ein neuer Kontakt"; @@ -371,6 +430,9 @@ swipe action */ /* alert title */ "Accept member" = "Mitglied annehmen"; +/* No comment provided by engineer. */ +"accepted" = "Angenommen"; + /* rcv group event chat item */ "accepted %@" = "%@ angenommen"; @@ -392,6 +454,9 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledgement errors" = "Fehler bei der Bestätigung"; +/* No comment provided by engineer. */ +"active" = "Aktiv"; + /* token status text */ "Active" = "Aktiv"; @@ -399,7 +464,7 @@ swipe action */ "Active connections" = "Aktive Verbindungen"; /* 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." = "Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet."; +"Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts." = "Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre SimpleX-Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre SimpleX-Kontakte gesendet."; /* No comment provided by engineer. */ "Add friends" = "Freunde aufnehmen"; @@ -440,6 +505,9 @@ swipe action */ /* No comment provided by engineer. */ "Added message servers" = "Nachrichtenserver hinzugefügt"; +/* No comment provided by engineer. */ +"Adding relays will be supported later." = "Das Hinzufügen von Relais wird zu einem späteren Zeitpunkt unterstützt."; + /* No comment provided by engineer. */ "Additional accent" = "Erste Akzentfarbe"; @@ -471,7 +539,7 @@ swipe action */ "Admins can block a member for all." = "Administratoren können ein Gruppenmitglied für Alle blockieren."; /* No comment provided by engineer. */ -"Admins can create the links to join groups." = "Administratoren können Links für den Beitritt zu Gruppen erzeugen."; +"Admins can create the links to join groups." = "Administratoren können Links für den Beitritt zu Gruppen erstellen."; /* No comment provided by engineer. */ "Advanced network settings" = "Erweiterte Netzwerkeinstellungen"; @@ -530,6 +598,12 @@ swipe action */ /* profile dropdown */ "All profiles" = "Alle Profile"; +/* No comment provided by engineer. */ +"All relays failed" = "Alle Relais fehlgeschlagen"; + +/* No comment provided by engineer. */ +"All relays removed" = "Alle Relais entfernt"; + /* No comment provided by engineer. */ "All reports will be archived for you." = "Alle Meldungen werden für Sie archiviert."; @@ -566,6 +640,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt. (24 Stunden)"; +/* No comment provided by engineer. */ +"Allow members to chat with admins." = "Mitgliedern den Chat mit Administratoren erlauben."; + /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "Erlauben Sie Reaktionen auf Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt."; @@ -575,12 +652,18 @@ swipe action */ /* No comment provided by engineer. */ "Allow sending direct messages to members." = "Das Senden von Direktnachrichten an Gruppenmitglieder erlauben."; +/* No comment provided by engineer. */ +"Allow sending direct messages to subscribers." = "Das Senden von Direktnachrichten an Abonnenten erlauben."; + /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Das Senden von verschwindenden Nachrichten erlauben."; /* No comment provided by engineer. */ "Allow sharing" = "Teilen erlauben"; +/* No comment provided by engineer. */ +"Allow subscribers to chat with admins." = "Abonnenten den Chat mit Administratoren erlauben."; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Unwiederbringliches löschen von gesendeten Nachrichten erlauben. (24 Stunden)"; @@ -650,9 +733,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Anruf annehmen"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "Jeder kann seine eigenen Server aufsetzen."; - /* No comment provided by engineer. */ "App build: %@" = "App Build: %@"; @@ -794,6 +874,15 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "Falsche Nachrichten-ID"; +/* No comment provided by engineer. */ +"Be free\nin your network" = "Seien Sie frei\nin Ihrem Netzwerk"; + +/* No comment provided by engineer. */ +"Be free in your network." = "Genießen Sie die Freiheit in Ihrem Netzwerk."; + +/* No comment provided by engineer. */ +"Because we destroyed the power to know who you are. So that your power can never be taken." = "Weil wir die Macht zerstört haben, zu wissen, wer Sie sind. Damit Ihnen Ihre Macht niemals genommen werden kann."; + /* No comment provided by engineer. */ "Better calls" = "Verbesserte Anrufe"; @@ -851,6 +940,9 @@ swipe action */ /* No comment provided by engineer. */ "Block member?" = "Mitglied blockieren?"; +/* No comment provided by engineer. */ +"Block subscriber for all?" = "Abonnent für alle blockieren?"; + /* marked deleted chat item preview text */ "blocked" = "Blockiert"; @@ -895,9 +987,15 @@ marked deleted chat item preview text */ "Both you and your contact can send voice messages." = "Sowohl Ihr Kontakt, als auch Sie können Sprachnachrichten senden."; /* 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)!" = "Bulgarisch, Finnisch, Thailändisch und Ukrainisch - Dank der Nutzer und [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +"Bottom bar" = "Untere Leiste"; + +/* compose placeholder for channel owner */ +"Broadcast" = "Broadcast"; /* 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)!" = "Bulgarisch, Finnisch, Thailändisch und Ukrainisch - Dank der Nutzer und [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + +/* chat link info line */ "Business address" = "Geschäftliche Adresse"; /* No comment provided by engineer. */ @@ -912,9 +1010,6 @@ marked deleted chat item preview text */ /* 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 Chat-Profil (Voreinstellung) oder [per Verbindung](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; -/* No comment provided by engineer. */ -"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "Durch die Nutzung von SimpleX Chat erklären Sie sich damit einverstanden:\n- nur legale Inhalte in öffentlichen Gruppen zu versenden.\n- andere Nutzer zu respektieren - kein Spam."; - /* No comment provided by engineer. */ "call" = "Anrufen"; @@ -939,6 +1034,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Camera not available" = "Kamera nicht verfügbar"; +/* No comment provided by engineer. */ +"can't broadcast" = "Broadcast nicht möglich"; + /* No comment provided by engineer. */ "Can't call contact" = "Kontakt kann nicht angerufen werden"; @@ -1038,6 +1136,58 @@ set passcode view */ /* chat item text */ "changing address…" = "Wechsel der Empfängeradresse wurde gestartet…"; +/* shown as sender role for channel messages */ +"channel" = "Kanal"; + +/* No comment provided by engineer. */ +"Channel" = "Kanal"; + +/* No comment provided by engineer. */ +"Channel display name" = "Anzeigename des Kanals"; + +/* No comment provided by engineer. */ +"Channel full name (optional)" = "Vollständiger Kanalname (optional)"; + +/* alert message +alert subtitle */ +"Channel has no active relays. Please try to join later." = "Der Kanal hat keine aktiven Relais. Bitte später erneut versuchen."; + +/* No comment provided by engineer. */ +"Channel image" = "Kanalbild"; + +/* chat link info line */ +"Channel link" = "Kanallink"; + +/* No comment provided by engineer. */ +"Channel preferences" = "Kanal-Präferenzen"; + +/* No comment provided by engineer. */ +"Channel profile" = "Kanalprofil"; + +/* No comment provided by engineer. */ +"Channel profile is stored on subscribers' devices and on the chat relays." = "Das Kanalprofil wird auf den Geräten der Abonnenten und auf den Chat‑Relais gespeichert."; + +/* snd group event chat item */ +"channel profile updated" = "Kanalprofil wurde aktualisiert"; + +/* alert message */ +"Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers." = "Das Kanalprofil wurde geändert. Beim Speichern wird das aktualisierte Profil an die Abonnenten des Kanals gesendet."; + +/* alert title */ +"Channel temporarily unavailable" = "Der Kanal ist vorübergehend nicht erreichbar"; + +/* No comment provided by engineer. */ +"Channel will be deleted for all subscribers - this cannot be undone!" = "Der Kanal wird für alle Abonnenten gelöscht. Dies kann nicht rückgängig gemacht werden!"; + +/* No comment provided by engineer. */ +"Channel will be deleted for you - this cannot be undone!" = "Der Kanal wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!"; + +/* alert message */ +"Channel will start working with %d of %d relays. Proceed?" = "Der Kanal wird mit %1$d von %2$d Relais gestartet. Fortfahren?"; + +/* No comment provided by engineer. */ +"Channels" = "Kanäle"; + /* No comment provided by engineer. */ "Chat" = "Chat"; @@ -1089,6 +1239,18 @@ set passcode view */ /* No comment provided by engineer. */ "Chat profile" = "Benutzerprofil"; +/* No comment provided by engineer. */ +"Chat relay" = "Chat-Relais"; + +/* No comment provided by engineer. */ +"Chat relays" = "Chat-Relais"; + +/* No comment provided by engineer. */ +"Chat relays forward messages in channels you create." = "Chat‑Relais leiten Nachrichten in den von Ihnen erstellten Kanälen weiter."; + +/* No comment provided by engineer. */ +"Chat relays forward messages to channel subscribers." = "Chat‑Relais leiten Nachrichten an Kanal-Abonnenten weiter."; + /* No comment provided by engineer. */ "Chat theme" = "Chat-Design"; @@ -1098,7 +1260,8 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "Der Chat wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!"; -/* chat toolbar */ +/* chat feature +chat toolbar */ "Chat with admins" = "Chat mit Administratoren"; /* No comment provided by engineer. */ @@ -1110,15 +1273,30 @@ set passcode view */ /* No comment provided by engineer. */ "Chats" = "Chats"; +/* No comment provided by engineer. */ +"Chats with admins are prohibited." = "Chats mit Administratoren sind nicht erlaubt."; + +/* alert message */ +"Chats with admins in public channels have no E2E encryption - use only with trusted chat relays." = "Chats mit Administratoren in öffentlichen Kanälen sind nicht Ende‑zu‑Ende‑verschlüsselt – bitte nur über vertrauenswürdige Chat‑Relais nutzen."; + /* No comment provided by engineer. */ "Chats with members" = "Chats mit Mitgliedern"; +/* No comment provided by engineer. */ +"Chats with members are disabled" = "Chats mit Mitgliedern sind deaktiviert"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Alle 20min Nachrichten überprüfen."; /* No comment provided by engineer. */ "Check messages when allowed." = "Wenn es erlaubt ist, Nachrichten überprüfen."; +/* alert message */ +"Check relay address and try again." = "Relais-Adresse überprüfen und erneut versuchen."; + +/* alert message */ +"Check relay name and try again." = "Relais-Name überprüfen und erneut versuchen."; + /* alert title */ "Check server address and try again." = "Überprüfen Sie die Serveradresse und versuchen Sie es nochmal."; @@ -1213,7 +1391,7 @@ set passcode view */ "Configure ICE servers" = "ICE-Server konfigurieren"; /* No comment provided by engineer. */ -"Configure server operators" = "Server-Betreiber konfigurieren"; +"Configure relays" = "Relais konfigurieren"; /* No comment provided by engineer. */ "Confirm" = "Bestätigen"; @@ -1248,7 +1426,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Bestätigt"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Verbinden"; /* No comment provided by engineer. */ @@ -1278,6 +1457,9 @@ set passcode view */ /* new chat sheet title */ "Connect via link" = "Über einen Link verbinden"; +/* No comment provided by engineer. */ +"Connect via link or QR code" = "Über einen Link oder QR-Code verbinden"; + /* new chat sheet title */ "Connect via one-time link" = "Über einen Einmal-Link verbinden"; @@ -1300,7 +1482,7 @@ set passcode view */ "Connected to desktop" = "Mit dem Desktop verbunden"; /* No comment provided by engineer. */ -"connecting" = "verbinde"; +"connecting" = "Verbinde"; /* No comment provided by engineer. */ "Connecting" = "Verbinden"; @@ -1315,7 +1497,7 @@ set passcode view */ "connecting (introduced)" = "Verbindung (erstellt)"; /* No comment provided by engineer. */ -"connecting (introduction invitation)" = "Verbinde (nach einer Einladung)"; +"connecting (introduction invitation)" = "Verbindung (nach einer Einladung)"; /* call status */ "connecting call" = "Anruf wird verbunden…"; @@ -1347,12 +1529,15 @@ set passcode view */ /* alert title */ "Connection error" = "Verbindungsfehler"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "Verbindungsfehler (AUTH)"; /* chat list item title (it should not be shown */ "connection established" = "Verbindung hergestellt"; +/* No comment provided by engineer. */ +"Connection failed" = "Verbindung fehlgeschlagen"; + /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "Die Verbindung wurde vom Serverbetreiber blockiert:\n%@"; @@ -1389,6 +1574,9 @@ set passcode view */ /* profile update event chat item */ "contact %@ changed to %@" = "Der Kontaktname wurde von %1$@ auf %2$@ geändert"; +/* chat link info line */ +"Contact address" = "Kontaktadresse"; + /* No comment provided by engineer. */ "Contact allows" = "Der Kontakt erlaubt"; @@ -1449,6 +1637,9 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "Weiter"; +/* No comment provided by engineer. */ +"Contribute" = "Unterstützen Sie uns"; + /* No comment provided by engineer. */ "Conversation deleted!" = "Chat-Inhalte entfernt!"; @@ -1464,17 +1655,14 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Abrundung Ecken"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Richtiger Name für %@?"; -/* No comment provided by engineer. */ -"Create" = "Erstellen"; - /* No comment provided by engineer. */ "Create 1-time link" = "Einmal-Link erstellen"; /* No comment provided by engineer. */ -"Create a group using a random profile." = "Erstellen Sie eine Gruppe mit einem zufälligen Profil."; +"Create a group using a random profile." = "Gruppe mit einem zufälligen Profil erstellen."; /* server test step */ "Create file" = "Datei erstellen"; @@ -1486,7 +1674,7 @@ set passcode view */ "Create group link" = "Gruppenlink erstellen"; /* No comment provided by engineer. */ -"Create link" = "Link erzeugen"; +"Create link" = "Link erstellen"; /* No comment provided by engineer. */ "Create list" = "Liste erstellen"; @@ -1497,8 +1685,14 @@ set passcode view */ /* No comment provided by engineer. */ "Create profile" = "Profil erstellen"; +/* No comment provided by engineer. */ +"Create public channel" = "Öffentlichen Kanal erstellen"; + +/* No comment provided by engineer. */ +"Create public channel (BETA)" = "Öffentlichen Kanal erstellen (BETA)"; + /* server test step */ -"Create queue" = "Erzeuge Warteschlange"; +"Create queue" = "Warteschlange erstellen"; /* No comment provided by engineer. */ "Create SimpleX address" = "SimpleX-Adresse erstellen"; @@ -1507,7 +1701,13 @@ set passcode view */ "Create your address" = "Ihre Adresse erstellen"; /* No comment provided by engineer. */ -"Create your profile" = "Erstellen Sie Ihr Profil"; +"Create your link" = "Ihren Link erstellen"; + +/* No comment provided by engineer. */ +"Create your profile" = "Ihr Profil erstellen"; + +/* No comment provided by engineer. */ +"Create your public address" = "Ihre öffentliche Adresse erstellen"; /* No comment provided by engineer. */ "Created" = "Erstellt"; @@ -1521,6 +1721,9 @@ set passcode view */ /* No comment provided by engineer. */ "Creating archive link" = "Archiv-Link erzeugen"; +/* No comment provided by engineer. */ +"Creating channel" = "Kanal wird erstellt"; + /* No comment provided by engineer. */ "Creating link…" = "Link wird erstellt…"; @@ -1623,8 +1826,8 @@ set passcode view */ /* No comment provided by engineer. */ "Debug delivery" = "Debugging-Zustellung"; -/* No comment provided by engineer. */ -"Decentralized" = "Dezentral"; +/* relay test step */ +"Decode link" = "Link dekodieren"; /* message decrypt error item */ "Decryption error" = "Entschlüsselungsfehler"; @@ -1667,6 +1870,12 @@ swipe action */ /* No comment provided by engineer. */ "Delete and notify contact" = "Kontakt löschen und benachrichtigen"; +/* No comment provided by engineer. */ +"Delete channel" = "Kanal löschen"; + +/* No comment provided by engineer. */ +"Delete channel?" = "Kanal löschen?"; + /* No comment provided by engineer. */ "Delete chat" = "Chat löschen"; @@ -1770,6 +1979,9 @@ alert button */ /* server test step */ "Delete queue" = "Lösche Warteschlange"; +/* No comment provided by engineer. */ +"Delete relay" = "Relais löschen"; + /* No comment provided by engineer. */ "Delete report" = "Meldung löschen"; @@ -1794,6 +2006,9 @@ alert button */ /* copied message info */ "Deleted at: %@" = "Gelöscht um: %@"; +/* rcv group event chat item */ +"deleted channel" = "Kanal gelöscht"; + /* rcv direct event chat item */ "deleted contact" = "Gelöschter Kontakt"; @@ -1884,6 +2099,12 @@ alert button */ /* No comment provided by engineer. */ "Direct messages between members are prohibited." = "In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt."; +/* No comment provided by engineer. */ +"Direct messages between subscribers are prohibited." = "Direktnachrichten zwischen Abonnenten sind nicht erlaubt."; + +/* alert button */ +"Disable" = "Deaktivieren"; + /* No comment provided by engineer. */ "Disable (keep overrides)" = "Deaktivieren (vorgenommene Einstellungen bleiben erhalten)"; @@ -1900,7 +2121,7 @@ alert button */ "Disable SimpleX Lock" = "SimpleX-Sperre deaktivieren"; /* No comment provided by engineer. */ -"disabled" = "deaktiviert"; +"disabled" = "Deaktiviert"; /* No comment provided by engineer. */ "Disabled" = "Deaktiviert"; @@ -1941,6 +2162,9 @@ alert button */ /* No comment provided by engineer. */ "Do not send history to new members." = "Den Nachrichtenverlauf nicht an neue Mitglieder senden."; +/* No comment provided by engineer. */ +"Do not send history to new subscribers." = "Den Nachrichtenverlauf nicht an neue Abonnenten senden."; + /* No comment provided by engineer. */ "Do NOT send messages directly, even if your or destination server does not support private routing." = "Nachrichten werden nicht direkt versendet, selbst wenn Ihr oder der Zielserver kein privates Routing unterstützt."; @@ -2020,27 +2244,39 @@ chat item action */ /* No comment provided by engineer. */ "E2E encrypted notifications." = "E2E-verschlüsselte Benachrichtigungen."; +/* No comment provided by engineer. */ +"Easier to invite your friends 👋" = "Freunde einladen – jetzt noch einfacher 👋"; + /* chat item action */ "Edit" = "Bearbeiten"; +/* No comment provided by engineer. */ +"Edit channel profile" = "Kanalprofil bearbeiten"; + /* No comment provided by engineer. */ "Edit group profile" = "Gruppenprofil bearbeiten"; /* No comment provided by engineer. */ "Empty message!" = "Leere Nachricht!"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "Aktivieren"; /* No comment provided by engineer. */ "Enable (keep overrides)" = "Aktivieren (vorgenommene Einstellungen bleiben erhalten)"; +/* channel creation warning */ +"Enable at least one chat relay in Network & Servers." = "Aktivieren Sie mindestens ein Chat‑Relais unter 'Netzwerk & Server'."; + /* alert title */ "Enable automatic message deletion?" = "Automatisches Löschen von Nachrichten aktivieren?"; /* No comment provided by engineer. */ "Enable camera access" = "Kamera-Zugriff aktivieren"; +/* alert title */ +"Enable chats with admins?" = "Chats mit Administratoren aktivieren?"; + /* No comment provided by engineer. */ "Enable disappearing messages by default." = "Verschwindende Nachrichten sind per Voreinstellung aktiviert."; @@ -2056,11 +2292,11 @@ chat item action */ /* No comment provided by engineer. */ "Enable instant notifications?" = "Sofortige Benachrichtigungen aktivieren?"; -/* No comment provided by engineer. */ -"Enable lock" = "Sperre aktivieren"; +/* alert title */ +"Enable link previews?" = "Linkvorschau aktivieren?"; /* No comment provided by engineer. */ -"Enable notifications" = "Benachrichtigungen aktivieren"; +"Enable lock" = "Sperre aktivieren"; /* No comment provided by engineer. */ "Enable periodic notifications?" = "Periodische Benachrichtigungen aktivieren?"; @@ -2167,6 +2403,9 @@ chat item action */ /* call status */ "ended call %@" = "Anruf beendet %@"; +/* No comment provided by engineer. */ +"Enter channel name…" = "Kanalname eingeben…"; + /* No comment provided by engineer. */ "Enter correct passphrase." = "Geben Sie das korrekte Passwort ein."; @@ -2185,6 +2424,12 @@ chat item action */ /* No comment provided by engineer. */ "Enter password above to show!" = "Für die Anzeige das Passwort im Suchfeld eingeben!"; +/* No comment provided by engineer. */ +"Enter profile name..." = "Profilname eingeben..."; + +/* No comment provided by engineer. */ +"Enter relay name…" = "Relais-Name eingeben…"; + /* No comment provided by engineer. */ "Enter server manually" = "Geben Sie den Server manuell ein"; @@ -2203,7 +2448,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "Fehler"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "Fehler"; /* No comment provided by engineer. */ @@ -2221,6 +2466,9 @@ chat item action */ /* No comment provided by engineer. */ "Error adding member(s)" = "Fehler beim Hinzufügen von Mitgliedern"; +/* alert title */ +"Error adding relay" = "Fehler beim Hinzufügen des Relais"; + /* alert title */ "Error adding server" = "Fehler beim Hinzufügen des Servers"; @@ -2257,6 +2505,9 @@ chat item action */ /* No comment provided by engineer. */ "Error creating address" = "Fehler beim Erstellen der Adresse"; +/* alert title */ +"Error creating channel" = "Fehler beim Erstellen des Kanals"; + /* No comment provided by engineer. */ "Error creating group" = "Fehler beim Erzeugen der Gruppe"; @@ -2338,9 +2589,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Fehler beim Öffnen des Chat"; -/* No comment provided by engineer. */ -"Error opening group" = "Fehler beim Vorbereiten der Gruppe"; - /* alert title */ "Error receiving file" = "Fehler beim Herunterladen der Datei"; @@ -2365,6 +2613,9 @@ chat item action */ /* No comment provided by engineer. */ "Error resetting statistics" = "Fehler beim Zurücksetzen der Statistiken"; +/* No comment provided by engineer. */ +"Error saving channel profile" = "Fehler beim Speichern des Kanalprofils"; + /* alert title */ "Error saving chat list" = "Fehler beim Speichern der Chat-Liste"; @@ -2407,6 +2658,9 @@ chat item action */ /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Fehler beim Setzen von Empfangsbestätigungen!"; +/* alert title */ +"Error sharing channel" = "Fehler beim Teilen des Kanals"; + /* No comment provided by engineer. */ "Error starting chat" = "Fehler beim Starten des Chats"; @@ -2449,12 +2703,16 @@ chat item action */ /* No comment provided by engineer. */ "Error: " = "Fehler: "; +/* receive error chat item */ +"error: %@" = "Fehler: %@"; + /* alert message file error text snd error text */ "Error: %@" = "Fehler: %@"; -/* server test error */ +/* relay test error +server test error */ "Error: %@." = "Fehler: %@."; /* No comment provided by engineer. */ @@ -2502,6 +2760,9 @@ snd error text */ /* No comment provided by engineer. */ "Exporting database archive…" = "Exportieren des Datenbank-Archivs…"; +/* No comment provided by engineer. */ +"failed" = "Fehlgeschlagen"; + /* No comment provided by engineer. */ "Failed to remove passphrase" = "Das Entfernen des Passworts ist fehlgeschlagen"; @@ -2604,7 +2865,8 @@ snd error text */ /* No comment provided by engineer. */ "Fingerprint in server address does not match certificate: %@." = "Fingerabdruck in der Serveradresse stimmt nicht mit dem Zertifikat überein: %@."; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Fingerabdruck in der Serveradresse stimmt nicht mit dem Zertifikat überein."; /* No comment provided by engineer. */ @@ -2628,7 +2890,11 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Für alle Moderatoren"; -/* servers error */ +/* No comment provided by engineer. */ +"For anyone to reach you" = "Damit Sie jeder erreichen kann"; + +/* servers error +servers warning */ "For chat profile %@:" = "Für das Chat-Profil %@:"; /* No comment provided by engineer. */ @@ -2712,9 +2978,15 @@ snd error text */ /* No comment provided by engineer. */ "Further reduced battery usage" = "Weiter reduzierter Batterieverbrauch"; +/* relay test step */ +"Get link" = "Link erhalten"; + /* No comment provided by engineer. */ "Get notified when mentioned." = "Bei Erwähnung benachrichtigt werden."; +/* No comment provided by engineer. */ +"Get started" = "Jetzt starten"; + /* No comment provided by engineer. */ "GIFs and stickers" = "GIFs und Sticker"; @@ -2760,7 +3032,7 @@ snd error text */ /* No comment provided by engineer. */ "group is deleted" = "Gruppe wurde gelöscht"; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "Gruppen-Link"; /* No comment provided by engineer. */ @@ -2832,6 +3104,9 @@ snd error text */ /* No comment provided by engineer. */ "History is not sent to new members." = "Der Nachrichtenverlauf wird nicht an neue Gruppenmitglieder gesendet."; +/* No comment provided by engineer. */ +"History is not sent to new subscribers." = "Der Nachrichtenverlauf wird nicht an neue Abonnenten gesendet."; + /* time unit */ "hours" = "Stunden"; @@ -2871,6 +3146,9 @@ snd error text */ /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "Wenn Sie Ihren Selbstzerstörungs-Zugangscode während des Öffnens der App eingeben:"; +/* down migration warning */ +"If you joined or created channels, they will stop working permanently." = "Kanäle, welche Sie erstellt haben oder denen Sie beigetreten sind, werden dauerhaft deaktiviert."; + /* 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)." = "Tippen Sie unten auf **Später wiederholen**, wenn Sie den Chat jetzt benötigen (es wird Ihnen angeboten, die Datenbank bei einem Neustart der App zu migrieren)."; @@ -2889,9 +3167,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "Sofort"; -/* No comment provided by engineer. */ -"Immune to spam" = "Immun gegen Spam und Missbrauch"; - /* No comment provided by engineer. */ "Import" = "Importieren"; @@ -2992,7 +3267,7 @@ snd error text */ "Initial role" = "Anfängliche Rolle"; /* No comment provided by engineer. */ -"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Installieren Sie [SimpleX Chat als Terminalanwendung](https://github.com/simplex-chat/simplex-chat)"; +"Install SimpleX Chat for terminal" = "Installieren Sie SimpleX Chat als Terminalanwendung"; /* No comment provided by engineer. */ "Instant" = "Sofort"; @@ -3027,7 +3302,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "Ungültige Chat-Daten"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "Ungültiger Verbindungslink"; /* invalid chat item */ @@ -3042,12 +3317,18 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Migrations-Bestätigung ungültig"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Ungültiger Name!"; /* No comment provided by engineer. */ "Invalid QR code" = "Ungültiger QR-Code"; +/* alert title */ +"Invalid relay address!" = "Ungültige Relais-Adresse!"; + +/* alert title */ +"Invalid relay name!" = "Ungültiger Relais-Name!"; + /* No comment provided by engineer. */ "Invalid response" = "Ungültige Reaktion"; @@ -3075,6 +3356,9 @@ snd error text */ /* No comment provided by engineer. */ "Invite members" = "Mitglieder einladen"; +/* No comment provided by engineer. */ +"Invite someone privately" = "Für privaten Chat einladen"; + /* No comment provided by engineer. */ "Invite to chat" = "Zum Chat einladen"; @@ -3141,6 +3425,9 @@ snd error text */ /* No comment provided by engineer. */ "Join as %@" = "Als %@ beitreten"; +/* No comment provided by engineer. */ +"Join channel" = "Kanal beitreten"; + /* new chat sheet title */ "Join group" = "Treten Sie der Gruppe bei"; @@ -3189,6 +3476,12 @@ snd error text */ /* swipe action */ "Leave" = "Verlassen"; +/* No comment provided by engineer. */ +"Leave channel" = "Kanal verlassen"; + +/* No comment provided by engineer. */ +"Leave channel?" = "Kanal verlassen?"; + /* No comment provided by engineer. */ "Leave chat" = "Chat verlassen"; @@ -3207,6 +3500,9 @@ snd error text */ /* No comment provided by engineer. */ "Less traffic on mobile networks." = "Weniger Datenverkehr in mobilen Netzen."; +/* No comment provided by engineer. */ +"Let someone connect to you" = "Jemand mit Ihnen verbinden lassen"; + /* email subject */ "Let's talk in SimpleX Chat" = "Lassen Sie uns in SimpleX Chat kommunizieren"; @@ -3216,9 +3512,15 @@ snd error text */ /* No comment provided by engineer. */ "Limitations" = "Einschränkungen"; +/* No comment provided by engineer. */ +"link" = "Link"; + /* No comment provided by engineer. */ "Link mobile and desktop apps! 🔗" = "Verknüpfe Mobiltelefon- und Desktop-Apps! 🔗"; +/* owner verification */ +"Link signature verified." = "Linksignatur erfolgreich überprüft."; + /* No comment provided by engineer. */ "Linked desktop options" = "Verknüpfte Desktop-Optionen"; @@ -3310,7 +3612,7 @@ snd error text */ "Member admission" = "Aufnahme von Mitgliedern"; /* rcv group event chat item */ -"member connected" = "ist der Gruppe beigetreten"; +"member connected" = "Verbunden"; /* No comment provided by engineer. */ "member has old version" = "Das Mitglied hat eine alte App-Version"; @@ -3348,6 +3650,9 @@ snd error text */ /* No comment provided by engineer. */ "Members can add message reactions." = "Gruppenmitglieder können eine Reaktion auf Nachrichten geben."; +/* No comment provided by engineer. */ +"Members can chat with admins." = "Mitglieder können mit Administratoren chatten."; + /* No comment provided by engineer. */ "Members can irreversibly delete sent messages. (24 hours)" = "Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden)"; @@ -3390,6 +3695,9 @@ snd error text */ /* No comment provided by engineer. */ "Message draft" = "Nachrichtenentwurf"; +/* No comment provided by engineer. */ +"Message error" = "Übertragungsfehler"; + /* item status text */ "Message forwarded" = "Nachricht weitergeleitet"; @@ -3450,6 +3758,12 @@ snd error text */ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Die Nachrichten von %@ werden angezeigt!"; +/* No comment provided by engineer. */ +"Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages." = "Nachrichten in diesem Kanal sind **nicht Ende‑zu‑Ende‑verschlüsselt**. Chat‑Relais können diese Nachrichten sehen."; + +/* E2EE info chat item */ +"Messages in this channel are not end-to-end encrypted. Chat relays can see these messages." = "Nachrichten in diesem Kanal sind nicht Ende‑zu‑Ende‑verschlüsselt. Chat‑Relais können diese Nachrichten sehen."; + /* alert message */ "Messages in this chat will never be deleted." = "Nachrichten in diesem Chat werden nie gelöscht."; @@ -3469,10 +3783,10 @@ snd error text */ "Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Nachrichten, Dateien und Anrufe sind durch **Quantum-resistente E2E-Verschlüsselung** mit Perfect Forward Secrecy, Abstreitbarkeit und Wiederherstellung nach einer Kompromittierung geschützt."; /* No comment provided by engineer. */ -"Migrate device" = "Gerät migrieren"; +"Migrate" = "Migrieren"; /* No comment provided by engineer. */ -"Migrate from another device" = "Von einem anderen Gerät migrieren"; +"Migrate device" = "Gerät migrieren"; /* No comment provided by engineer. */ "Migrate here" = "Hierher migrieren"; @@ -3564,12 +3878,18 @@ snd error text */ /* No comment provided by engineer. */ "Network & servers" = "Netzwerk & Server"; +/* No comment provided by engineer. */ +"Network commitments" = "Netzwerk Verpflichtungen"; + /* No comment provided by engineer. */ "Network connection" = "Netzwerkverbindung"; /* No comment provided by engineer. */ "Network decentralization" = "Dezentralisiertes Netzwerk"; +/* conn error description */ +"Network error" = "Netzwerk-Fehler"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "Netzwerk-Fehler - die Nachricht ist nach vielen Sende-Versuchen abgelaufen."; @@ -3579,6 +3899,9 @@ snd error text */ /* No comment provided by engineer. */ "Network operator" = "Netzwerk-Betreiber"; +/* No comment provided by engineer. */ +"Network routers cannot know\nwho talks to whom" = "Netzwerk‑Router können nicht erkennen,\nwer mit wem kommuniziert"; + /* No comment provided by engineer. */ "Network settings" = "Netzwerkeinstellungen"; @@ -3588,15 +3911,24 @@ snd error text */ /* delete after time */ "never" = "nie"; +/* No comment provided by engineer. */ +"new" = "Neu"; + /* token status text */ "New" = "Neu"; +/* No comment provided by engineer. */ +"New 1-time link" = "Neuer Einmal-Link"; + /* No comment provided by engineer. */ "New chat" = "Neuer Chat"; /* No comment provided by engineer. */ "New chat experience 🎉" = "Neue Chat-Erfahrung 🎉"; +/* No comment provided by engineer. */ +"New chat relay" = "Neues Chat-Relais"; + /* notification */ "New contact request" = "Neue Kontaktanfrage"; @@ -3654,9 +3986,21 @@ snd error text */ /* No comment provided by engineer. */ "No" = "Nein"; +/* No comment provided by engineer. */ +"No account. No phone. No email. No ID.\nThe most secure encryption." = "Kein Account. Keine Telefonnummer. Keine E‑Mail. Keine ID.\nDie sicherste Verschlüsselung."; + +/* No comment provided by engineer. */ +"No active relays" = "Keine aktiven Relais"; + /* Authentication unavailable */ "No app password" = "Kein App-Passwort"; +/* No comment provided by engineer. */ +"No chat relays" = "Keine Chat-Relais"; + +/* servers warning */ +"No chat relays enabled." = "Es sind keine Chat-Relais aktiviert."; + /* No comment provided by engineer. */ "No chats" = "Keine Chats"; @@ -3754,7 +4098,16 @@ snd error text */ "No unread chats" = "Keine ungelesenen Chats"; /* No comment provided by engineer. */ -"No user identifiers." = "Keine Benutzerkennungen."; +"Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "Niemand verfolgte Ihre Gespräche. Niemand erstellte eine Karte, wo Sie sich aufgehalten haben. Privatsphäre war nie ein Feature - sie war selbstverständlich."; + +/* No comment provided by engineer. */ +"Non-profit governance" = "Non‑Profit‑Governance"; + +/* No comment provided by engineer. */ +"Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "Nicht ein besseres Schloss an der Tür eines Anderen. Kein freundlicher Vermieter, der Ihre Privatsphäre respektiert, aber dennoch jeden Besucher registriert. Sie sind kein Gast. Sie sind zu Hause. Kein Vermieter, kein Fremder kann es betreten - Sie sind souverän."; + +/* alert title */ +"Not all relays connected" = "Es sind nicht alle Relais verbunden"; /* No comment provided by engineer. */ "Not compatible!" = "Nicht kompatibel!"; @@ -3812,7 +4165,7 @@ alert button new chat action */ "Ok" = "Ok"; -/* No comment provided by engineer. */ +/* alert button */ "OK" = "OK"; /* No comment provided by engineer. */ @@ -3821,9 +4174,15 @@ new chat action */ /* group pref value */ "on" = "Ein"; +/* No comment provided by engineer. */ +"On your phone, not on servers." = "Auf Ihrem Gerät, nicht auf Servern."; + /* No comment provided by engineer. */ "One-time invitation link" = "Einmal-Einladungslink"; +/* chat link info line */ +"One-time link" = "Einmal-Link"; + /* No comment provided by engineer. */ "Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Für diese Verbindung werden Onion-Hosts benötigt.\nDies erfordert die Aktivierung eines VPNs."; @@ -3834,7 +4193,10 @@ new chat action */ "Onion hosts will not be used." = "Onion-Hosts werden nicht verwendet."; /* No comment provided by engineer. */ -"Only chat owners can change preferences." = "Nur Chat-Eigentümer können die Präferenzen ändern."; +"Only channel owners can change channel preferences." = "Kanal-Präferenzen können nur von Kanal-Eigentümern geändert werden."; + +/* No comment provided by engineer. */ +"Only chat owners can change preferences." = "Präferenzen können nur von Chat-Eigentümern geändert werden."; /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages." = "Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine **2-Schichten Ende-zu-Ende-Verschlüsselung** gesendet werden."; @@ -3893,12 +4255,16 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Nur Ihr Kontakt kann Sprachnachrichten versenden."; -/* alert action */ +/* alert action +alert button */ "Open" = "Öffnen"; /* No comment provided by engineer. */ "Open changes" = "Änderungen öffnen"; +/* new chat action */ +"Open channel" = "Kanal öffnen"; + /* new chat action */ "Open chat" = "Chat öffnen"; @@ -3911,6 +4277,9 @@ new chat action */ /* No comment provided by engineer. */ "Open conditions" = "Nutzungsbedingungen öffnen"; +/* alert title */ +"Open external link?" = "Externen Link öffnen?"; + /* alert action */ "Open full link" = "Vollständigen Link öffnen"; @@ -3923,6 +4292,9 @@ new chat action */ /* authentication reason */ "Open migration to another device" = "Migration auf ein anderes Gerät öffnen"; +/* new chat action */ +"Open new channel" = "Neuen Kanal öffnen"; + /* new chat action */ "Open new chat" = "Neuen Chat öffnen"; @@ -3953,6 +4325,9 @@ new chat action */ /* alert title */ "Operator server" = "Betreiber-Server"; +/* No comment provided by engineer. */ +"Operators commit to:\n- Be independent\n- Minimize metadata usage\n- Run verified open-source code" = "Betreiber verpflichten sich:\n- Unabhängig zu bleiben\n- Metadaten auf ein Minimum zu reduzieren\n- Geprüften Open‑Source‑Code einzusetzen"; + /* No comment provided by engineer. */ "Or import archive file" = "Oder importieren Sie eine Archiv-Datei"; @@ -3965,12 +4340,18 @@ new chat action */ /* No comment provided by engineer. */ "Or securely share this file link" = "Oder teilen Sie diesen Datei-Link sicher"; +/* No comment provided by engineer. */ +"Or show QR in person or via video call." = "Oder den QR‑Code persönlich oder per Videoanruf zeigen."; + /* No comment provided by engineer. */ "Or show this code" = "Oder diesen QR-Code anzeigen"; /* No comment provided by engineer. */ "Or to share privately" = "Oder zum privaten Teilen"; +/* No comment provided by engineer. */ +"Or use this QR - print or show online." = "Oder diesen QR‑Code verwenden – ausgedruckt oder online."; + /* No comment provided by engineer. */ "Organize chats into lists" = "Chats in Listen verwalten"; @@ -3989,9 +4370,18 @@ new chat action */ /* member role */ "owner" = "Eigentümer"; +/* No comment provided by engineer. */ +"Owner" = "Eigentümer"; + /* feature role */ "owners" = "Eigentümer"; +/* No comment provided by engineer. */ +"Owners" = "Eigentümer"; + +/* No comment provided by engineer. */ +"Ownership: you can run your own relays." = "Volle Kontrolle: Sie können Ihre eigenen Relais betreiben."; + /* No comment provided by engineer. */ "Passcode" = "Zugangscode"; @@ -4019,6 +4409,9 @@ new chat action */ /* No comment provided by engineer. */ "Paste image" = "Bild einfügen"; +/* No comment provided by engineer. */ +"Paste link / Scan" = "Link einfügen / Scannen"; + /* No comment provided by engineer. */ "Paste link to connect!" = "Zum Verbinden den Link einfügen!"; @@ -4127,6 +4520,12 @@ new chat action */ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren."; +/* No comment provided by engineer. */ +"Preset relay address" = "Voreingestellte Relais-Adresse"; + +/* No comment provided by engineer. */ +"Preset relay name" = "Voreingestellter Relais-Name"; + /* No comment provided by engineer. */ "Preset server address" = "Voreingestellte Serveradresse"; @@ -4149,10 +4548,10 @@ new chat action */ "Privacy policy and conditions of use." = "Datenschutz- und Nutzungsbedingungen."; /* No comment provided by engineer. */ -"Privacy redefined" = "Datenschutz neu definiert"; +"Privacy: for owners and subscribers." = "Privatsphäre: für Besitzer und Abonnenten."; /* No comment provided by engineer. */ -"Private chats, groups and your contacts are not accessible to server operators." = "Private Chats, Gruppen und Ihre Kontakte sind für Server-Betreiber nicht zugänglich."; +"Private and secure messaging." = "Private und sichere Kommunikation."; /* No comment provided by engineer. */ "Private filenames" = "Neutrale Dateinamen"; @@ -4178,6 +4577,9 @@ new chat action */ /* alert title */ "Private routing timeout" = "Zeitüberschreitung der privaten Routing-Sitzung"; +/* alert action */ +"Proceed" = "Fortfahren"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil und Serververbindungen"; @@ -4194,11 +4596,14 @@ new chat action */ "Profile theme" = "Profil-Design"; /* alert message */ -"Profile update will be sent to your contacts." = "Profil-Aktualisierung wird an Ihre Kontakte gesendet."; +"Profile update will be sent to your SimpleX contacts." = "Profil-Aktualisierung wird an Ihre SimpleX-Kontakte gesendet."; /* No comment provided by engineer. */ "Prohibit audio/video calls." = "Audio-/Video-Anrufe nicht erlauben."; +/* No comment provided by engineer. */ +"Prohibit chats with admins." = "Chat mit Administratoren nicht erlauben."; + /* No comment provided by engineer. */ "Prohibit irreversible message deletion." = "Unwiederbringliches löschen von Nachrichten nicht erlauben."; @@ -4214,6 +4619,9 @@ new chat action */ /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "Das Senden von Direktnachrichten an Gruppenmitglieder nicht erlauben."; +/* No comment provided by engineer. */ +"Prohibit sending direct messages to subscribers." = "Das Senden von Direktnachrichten an Abonnenten nicht erlauben."; + /* No comment provided by engineer. */ "Prohibit sending disappearing messages." = "Das Senden von verschwindenden Nachrichten nicht erlauben."; @@ -4256,6 +4664,9 @@ new chat action */ /* No comment provided by engineer. */ "Proxy requires password" = "Der Proxy benötigt ein Passwort"; +/* No comment provided by engineer. */ +"Public channels - speak freely 🚀" = "Öffentliche Kanäle – frei sprechen 🚀"; + /* No comment provided by engineer. */ "Push notifications" = "Push-Benachrichtigungen"; @@ -4284,16 +4695,10 @@ new chat action */ "Read more" = "Mehr erfahren"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Lesen Sie mehr dazu im [Benutzerhandbuch](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in our GitHub repository." = "Erfahren Sie in unserem GitHub-Repository mehr dazu."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) lesen."; - -/* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/readme.html#connect-to-friends) lesen."; - -/* No comment provided by engineer. */ -"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Erfahren Sie in unserem [GitHub-Repository](https://github.com/simplex-chat/simplex-chat#readme) mehr dazu."; +"Read more in User Guide." = "Lesen Sie mehr dazu im Benutzerhandbuch."; /* No comment provided by engineer. */ "Receipts are disabled" = "Bestätigungen sind deaktiviert"; @@ -4402,12 +4807,36 @@ swipe action */ /* call status */ "rejected call" = "Abgelehnter Anruf"; +/* member role */ +"relay" = "Relais"; + +/* No comment provided by engineer. */ +"Relay" = "Relais"; + +/* alert title */ +"Relay address" = "Relais-Adresse"; + +/* alert title */ +"Relay connection failed" = "Relais-Verbindung fehlgeschlagen"; + +/* No comment provided by engineer. */ +"Relay link" = "Relais-Link"; + +/* alert message */ +"Relay results:" = "Relay‑Status:"; + /* No comment provided by engineer. */ "Relay server is only used if necessary. Another party can observe your IP address." = "Relais-Server werden nur genutzt, wenn sie benötigt werden. Ihre IP-Adresse kann von Anderen erfasst werden."; /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "Relais-Server schützen Ihre IP-Adresse, aber sie können die Anrufdauer erfassen."; +/* No comment provided by engineer. */ +"Relay test failed!" = "Relais-Test fehlgeschlagen!"; + +/* No comment provided by engineer. */ +"Reliability: many relays per channel." = "Zuverlässigkeit: mehrere Relais pro Kanal."; + /* alert action */ "Remove" = "Entfernen"; @@ -4432,12 +4861,24 @@ swipe action */ /* No comment provided by engineer. */ "Remove passphrase from keychain?" = "Passwort aus dem Schlüsselbund entfernen?"; +/* No comment provided by engineer. */ +"Remove subscriber" = "Abonnent entfernen"; + +/* alert title */ +"Remove subscriber?" = "Abonnent entfernen?"; + /* No comment provided by engineer. */ "removed" = "entfernt"; +/* receive error chat item */ +"removed (%d attempts)" = "Entfernt (%d Versuche)"; + /* rcv group event chat item */ "removed %@" = "hat %@ aus der Gruppe entfernt"; +/* No comment provided by engineer. */ +"removed by operator" = "Vom Betreiber entfernt"; + /* profile update event chat item */ "removed contact address" = "Die Kontaktadresse wurde entfernt"; @@ -4606,6 +5047,9 @@ swipe action */ /* No comment provided by engineer. */ "Run chat" = "Chat starten"; +/* No comment provided by engineer. */ +"Safe web links" = "Sichere Web-Links"; + /* No comment provided by engineer. */ "Safely receive files" = "Dateien sicher herunterladen"; @@ -4622,6 +5066,9 @@ chat item action */ /* alert button */ "Save (and notify members)" = "Speichern (und Mitglieder benachrichtigen)"; +/* alert button */ +"Save (and notify subscribers)" = "Speichern (Abonnenten benachrichtigen)"; + /* alert title */ "Save admission settings?" = "Speichern der Aufnahme-Einstellungen?"; @@ -4631,12 +5078,21 @@ chat item action */ /* No comment provided by engineer. */ "Save and notify group members" = "Speichern und Gruppenmitglieder benachrichtigen"; +/* No comment provided by engineer. */ +"Save and notify subscribers" = "Speichern und Abonnenten benachrichtigen"; + /* No comment provided by engineer. */ "Save and reconnect" = "Speichern und neu verbinden"; /* No comment provided by engineer. */ "Save and update group profile" = "Gruppen-Profil sichern und aktualisieren"; +/* No comment provided by engineer. */ +"Save channel profile" = "Kanalprofil speichern"; + +/* alert title */ +"Save channel profile?" = "Kanalprofil speichern?"; + /* No comment provided by engineer. */ "Save group profile" = "Gruppenprofil speichern"; @@ -4766,6 +5222,9 @@ chat item action */ /* chat item text */ "security code changed" = "Sicherheitscode wurde geändert"; +/* No comment provided by engineer. */ +"Security: owners hold channel keys." = "Sicherheit: Eigentümer besitzen die Kanalschlüssel."; + /* chat item action */ "Select" = "Auswählen"; @@ -4776,7 +5235,7 @@ chat item action */ "Selected %lld" = "%lld ausgewählt"; /* No comment provided by engineer. */ -"Selected chat preferences prohibit this message." = "Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt."; +"Selected chat preferences prohibit this message." = "Diese Nachricht ist wegen der gewählten Chat-Präferenzen nicht erlaubt."; /* No comment provided by engineer. */ "Self-destruct" = "Selbstzerstörung"; @@ -4812,7 +5271,7 @@ chat item action */ "Send errors" = "Fehler beim Senden"; /* No comment provided by engineer. */ -"Send link previews" = "Link-Vorschau senden"; +"Send link previews" = "Linkvorschau senden"; /* No comment provided by engineer. */ "Send live message" = "Live Nachricht senden"; @@ -4844,12 +5303,18 @@ chat item action */ /* No comment provided by engineer. */ "Send request without message" = "Anfrage ohne Nachricht senden"; +/* No comment provided by engineer. */ +"Send the link via any messenger - it's secure. Ask to paste into SimpleX." = "Den Link über einen beliebigen Messenger versenden – es ist sicher. Bitte in SimpleX einfügen."; + /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Senden Sie diese aus dem Fotoalbum oder von individuellen Tastaturen."; /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Bis zu 100 der letzten Nachrichten an neue Gruppenmitglieder senden."; +/* No comment provided by engineer. */ +"Send up to 100 last messages to new subscribers." = "Bis zu 100 der letzten Nachrichten an neue Abonnenten senden."; + /* No comment provided by engineer. */ "Send your private feedback to groups." = "Senden Sie Ihr privates Feedback an Gruppen."; @@ -4859,6 +5324,9 @@ chat item action */ /* No comment provided by engineer. */ "Sender may have deleted the connection request." = "Der Absender hat möglicherweise die Verbindungsanfrage gelöscht."; +/* alert message */ +"Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later." = "Das Senden einer Link-Vorschau kann Ihre IP‑Adresse an die Website übermitteln. Sie können dies später in den Datenschutzeinstellungen ändern."; + /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Das Senden von Empfangsbestätigungen an alle Kontakte in allen sichtbaren Chat-Profilen wird aktiviert."; @@ -4937,6 +5405,9 @@ chat item action */ /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "Server-Warteschlangen-Information: %1$@\n\nZuletzt empfangene Nachricht: %2$@"; +/* relay test error */ +"Server requires authorization to connect to relay, check password." = "Der Server erfordert eine Autorisierung, um eine Verbindung zum Relais herzustellen. Bitte Passwort überprüfen."; + /* server test error */ "Server requires authorization to create queues, check password." = "Der Server erfordert zum Erstellen von Warteschlangen eine Autorisierung. Bitte überprüfen Sie das Passwort."; @@ -5021,6 +5492,12 @@ chat item action */ /* alert message */ "Settings were changed." = "Die Einstellungen wurden geändert."; +/* No comment provided by engineer. */ +"Setup notifications" = "Benachrichtigungen einrichten"; + +/* No comment provided by engineer. */ +"Setup routers" = "Router einrichten"; + /* No comment provided by engineer. */ "Shape profile images" = "Form der Profil-Bilder"; @@ -5041,7 +5518,10 @@ chat item action */ "Share address publicly" = "Die Adresse öffentlich teilen"; /* alert title */ -"Share address with contacts?" = "Die Adresse mit Kontakten teilen?"; +"Share address with SimpleX contacts?" = "Die Adresse mit SimpleX-Kontakten teilen?"; + +/* No comment provided by engineer. */ +"Share channel" = "Kanal teilen"; /* No comment provided by engineer. */ "Share from other apps." = "Aus anderen Apps heraus teilen."; @@ -5058,6 +5538,9 @@ chat item action */ /* No comment provided by engineer. */ "Share profile" = "Profil teilen"; +/* No comment provided by engineer. */ +"Share relay address" = "Relais-Adresse teilen"; + /* No comment provided by engineer. */ "Share SimpleX address on social media." = "Die SimpleX-Adresse auf sozialen Medien teilen."; @@ -5068,7 +5551,10 @@ chat item action */ "Share to SimpleX" = "Mit SimpleX teilen"; /* No comment provided by engineer. */ -"Share with contacts" = "Mit Kontakten teilen"; +"Share via chat" = "Per Chat teilen"; + +/* No comment provided by engineer. */ +"Share with SimpleX contacts" = "Mit SimpleX-Kontakten teilen"; /* No comment provided by engineer. */ "Share your address" = "Ihre Adresse teilen"; @@ -5128,7 +5614,7 @@ chat item action */ "SimpleX address settings" = "Einstellungen automatisch akzeptieren"; /* simplex link type */ -"SimpleX channel link" = "SimpleX-Kanal-Link"; +"SimpleX channel link" = "SimpleX-Kanallink"; /* No comment provided by engineer. */ "SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX-Chat und Flux haben vereinbart, die von Flux betriebenen Server in die App aufzunehmen."; @@ -5173,7 +5659,7 @@ chat item action */ "SimpleX protocols reviewed by Trail of Bits." = "Die SimpleX-Protokolle wurden von Trail of Bits überprüft."; /* simplex link type */ -"SimpleX relay link" = "SimpleX Relais-Link"; +"SimpleX relay address" = "SimpleX Relais-Adresse"; /* No comment provided by engineer. */ "Simplified incognito mode" = "Vereinfachter Inkognito-Modus"; @@ -5227,6 +5713,9 @@ report reason */ /* chat item text */ "standard end-to-end encryption" = "Standard-Ende-zu-Ende-Verschlüsselung"; +/* No comment provided by engineer. */ +"Star on GitHub" = "Stern auf GitHub vergeben"; + /* No comment provided by engineer. */ "Start chat" = "Starten Sie den Chat"; @@ -5293,6 +5782,48 @@ report reason */ /* No comment provided by engineer. */ "Subscribed" = "Abonniert"; +/* No comment provided by engineer. */ +"Subscriber" = "Abonnent"; + +/* chat feature */ +"Subscriber reports" = "Abonnenten-Meldungen"; + +/* alert message */ +"Subscriber will be removed from channel - this cannot be undone!" = "Abonnent wird aus dem Kanal entfernt. Dies kann nicht rückgängig gemacht werden!"; + +/* No comment provided by engineer. */ +"Subscribers" = "Abonnenten"; + +/* No comment provided by engineer. */ +"Subscribers can add message reactions." = "Abonnenten können eine Reaktion auf Nachrichten geben."; + +/* No comment provided by engineer. */ +"Subscribers can chat with admins." = "Abonnenten können mit Administratoren chatten."; + +/* No comment provided by engineer. */ +"Subscribers can irreversibly delete sent messages. (24 hours)" = "Abonnenten können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden)"; + +/* No comment provided by engineer. */ +"Subscribers can report messsages to moderators." = "Abonnenten können Nachrichten an Moderatoren melden."; + +/* No comment provided by engineer. */ +"Subscribers can send direct messages." = "Abonnenten können Direktnachrichten versenden."; + +/* No comment provided by engineer. */ +"Subscribers can send disappearing messages." = "Abonnenten können verschwindende Nachrichten versenden."; + +/* No comment provided by engineer. */ +"Subscribers can send files and media." = "Abonnenten können Dateien und Medien versenden."; + +/* No comment provided by engineer. */ +"Subscribers can send SimpleX links." = "Abonnenten können SimpleX-Links versenden."; + +/* No comment provided by engineer. */ +"Subscribers can send voice messages." = "Abonnenten können Sprachnachrichten versenden."; + +/* No comment provided by engineer. */ +"Subscribers use relay link to connect to the channel.\nRelay address was used to set up this relay for the channel." = "Abonnenten verbinden sich über den Relais‑Link mit dem Kanal.\nDie Relais-Adresse wurde zur Einrichtung dieses Relais für diesen Kanal verwendet."; + /* No comment provided by engineer. */ "Subscription errors" = "Fehler beim Abonnieren"; @@ -5320,6 +5851,9 @@ report reason */ /* No comment provided by engineer. */ "Take picture" = "Machen Sie ein Foto"; +/* No comment provided by engineer. */ +"Talk to someone" = "Mit jemandem sprechen"; + /* No comment provided by engineer. */ "Tap button " = "Schaltfläche antippen "; @@ -5333,13 +5867,13 @@ report reason */ "Tap Connect to use bot" = "Verbinden tippen, um den Bot zu nutzen."; /* No comment provided by engineer. */ -"Tap Create SimpleX address in the menu to create it later." = "Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen."; +"Tap Join channel" = "Tippen, um dem Kanal beizutreten"; /* No comment provided by engineer. */ "Tap Join group" = "Tippen, um der Gruppe beizutreten"; /* No comment provided by engineer. */ -"Tap to activate profile." = "Zum Aktivieren des Profils tippen."; +"Tap to activate profile." = "Tippen, um das Profil zu aktivieren."; /* No comment provided by engineer. */ "Tap to Connect" = "Zum Verbinden tippen"; @@ -5351,7 +5885,10 @@ report reason */ "Tap to join incognito" = "Zum Inkognito beitreten tippen"; /* No comment provided by engineer. */ -"Tap to paste link" = "Zum Link einfügen tippen"; +"Tap to open" = "Zum Öffnen tippen"; + +/* No comment provided by engineer. */ +"Tap to paste link" = "Tippen, um den Link einzufügen"; /* No comment provided by engineer. */ "Tap to scan" = "Zum Scannen tippen"; @@ -5380,12 +5917,16 @@ report reason */ /* file error alert title */ "Temporary file error" = "Temporärer Datei-Fehler"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Der Test ist beim Schritt %@ fehlgeschlagen."; /* No comment provided by engineer. */ "Test notifications" = "Benachrichtigungen testen"; +/* No comment provided by engineer. */ +"Test relay" = "Relais testen"; + /* No comment provided by engineer. */ "Test server" = "Teste Server"; @@ -5413,6 +5954,9 @@ report reason */ /* No comment provided by engineer. */ "The app protects your privacy by using different operators in each conversation." = "Durch Verwendung verschiedener Netzwerk-Betreiber für jede Unterhaltung schützt die App Ihre Privatsphäre."; +/* No comment provided by engineer. */ +"The app removed this message after %lld attempts to receive it." = "Die App hat diese Nachricht nach %lld Empfangsversuchen entfernt."; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "Die App wird eine Bestätigung bei Downloads von unbekannten Datei-Servern anfordern (außer bei .onion)."; @@ -5422,6 +5966,9 @@ report reason */ /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "Der von Ihnen gescannte Code ist kein SimpleX-Link-QR-Code."; +/* conn error description */ +"The connection reached the limit of undelivered messages" = "Die Verbindung hat das Limit für nicht zugestellte Nachrichten erreicht"; + /* No comment provided by engineer. */ "The connection reached the limit of undelivered messages, your contact may be offline." = "Diese Verbindung hat das Limit der nicht ausgelieferten Nachrichten erreicht. Ihr Kontakt ist möglicherweise offline."; @@ -5438,7 +5985,7 @@ report reason */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Die Verschlüsselung funktioniert und ein neues Verschlüsselungsabkommen ist nicht erforderlich. Es kann zu Verbindungsfehlern kommen!"; /* No comment provided by engineer. */ -"The future of messaging" = "Die nächste Generation von privatem Messaging"; +"The first network where you own\nyour contacts and groups." = "Das erste Netzwerk,\nin dem Sie Ihre Kontakte und Gruppen besitzen."; /* No comment provided by engineer. */ "The hash of the previous message is different." = "Der Hash der vorherigen Nachricht unterscheidet sich."; @@ -5464,6 +6011,9 @@ report reason */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Die alte Datenbank wurde während der Migration nicht entfernt. Sie kann gelöscht werden."; +/* No comment provided by engineer. */ +"The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it." = "Die älteste Freiheit des Menschen - mit einem anderen Menschen sprechen zu können, ohne beobachtet zu werden - gestützt auf einer Infrastruktur, die Sie nicht verraten kann."; + /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**."; @@ -5491,6 +6041,12 @@ report reason */ /* No comment provided by engineer. */ "Themes" = "Design"; +/* No comment provided by engineer. */ +"Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible." = "Dann sind wir online gegangen, und jede Plattform wollte Etwas von Ihnen - Ihren Namen, Ihre Nummer, Ihre Freunde. Wir akzeptierten, dass es der Preis mit Anderen zu kommunizieren ist, Jemandem preiszugeben, mit wem und wie wir miteinander kommunizieren. Jede Generation, Menschen und Technologien, kannten es nur so - Telefon, E-Mail, Messenger, soziale Medien. Es schien der einzig mögliche Weg zu sein."; + +/* No comment provided by engineer. */ +"There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected." = "Es gibt einen anderen Weg. Ein Netzwerk ohne Telefonnummern, ohne Benutzernamen, ohne Benutzerkennungen und ohne jegliche Benutzeridentität. Ein Netzwerk, welches Menschen verbindet und verschlüsselte Nachrichten überträgt, ohne zu wissen, wer mit wem verbunden ist."; + /* No comment provided by engineer. */ "These conditions will also apply for: **%@**." = "Diese Nutzungsbedingungen gelten auch für: **%@**."; @@ -5533,6 +6089,12 @@ report reason */ /* No comment provided by engineer. */ "This group no longer exists." = "Diese Gruppe existiert nicht mehr."; +/* alert message */ +"This is a chat relay address, it cannot be used to connect." = "Dies ist eine Chat‑Relais-Adresse, welche nicht zum Verbinden verwendet werden kann."; + +/* new chat action */ +"This is your link for channel %@!" = "Dies ist Ihr Link für den Kanal %@!"; + /* No comment provided by engineer. */ "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Für diesen Link wird eine neuere App-Version benötigt. Bitte aktualisieren Sie die App oder bitten Sie Ihren Kontakt einen kompatiblen Link zu senden."; @@ -5566,6 +6128,9 @@ report reason */ /* No comment provided by engineer. */ "To make a new connection" = "Um eine Verbindung mit einem neuen Kontakt zu erstellen"; +/* No comment provided by engineer. */ +"To make SimpleX Network last." = "Für ein dauerhaftes SimpleX-Netzwerk."; + /* No comment provided by engineer. */ "To protect against your link being replaced, you can compare contact security codes." = "Zum Schutz vor dem Austausch Ihres Links können Sie die Sicherheitscodes Ihrer Kontakte vergleichen."; @@ -5614,9 +6179,6 @@ report reason */ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen."; -/* No comment provided by engineer. */ -"Toggle chat list:" = "Chat-Liste umschalten:"; - /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Inkognito beim Verbinden einschalten."; @@ -5626,6 +6188,9 @@ report reason */ /* No comment provided by engineer. */ "Toolbar opacity" = "Deckkraft der Symbolleiste"; +/* No comment provided by engineer. */ +"Top bar" = "Obere Leiste"; + /* No comment provided by engineer. */ "Total" = "Summe aller Abonnements"; @@ -5665,6 +6230,9 @@ report reason */ /* No comment provided by engineer. */ "Unblock member?" = "Mitglied freigeben?"; +/* No comment provided by engineer. */ +"Unblock subscriber for all?" = "Abonnent für alle freigeben?"; + /* rcv group event chat item */ "unblocked %@" = "hat %@ freigegeben"; @@ -5737,12 +6305,15 @@ report reason */ /* swipe action */ "Unread" = "Ungelesen"; -/* No comment provided by engineer. */ +/* conn error description */ "Unsupported connection link" = "Verbindungs-Link wird nicht unterstützt"; /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Bis zu 100 der letzten Nachrichten werden an neue Mitglieder gesendet."; +/* No comment provided by engineer. */ +"Up to 100 last messages are sent to new subscribers." = "Bis zu 100 der letzten Nachrichten werden an neue Abonnenten gesendet."; + /* No comment provided by engineer. */ "Update" = "Aktualisieren"; @@ -5755,6 +6326,9 @@ report reason */ /* No comment provided by engineer. */ "Update settings?" = "Einstellungen aktualisieren?"; +/* rcv group event chat item */ +"updated channel profile" = "Kanalprofil aktualisiert"; + /* No comment provided by engineer. */ "Updated conditions" = "Aktualisierte Nutzungsbedingungen"; @@ -5812,9 +6386,6 @@ report reason */ /* No comment provided by engineer. */ "Use %@" = "Verwende %@"; -/* No comment provided by engineer. */ -"Use chat" = "Verwenden Sie Chat"; - /* new chat action */ "Use current profile" = "Aktuelles Profil nutzen"; @@ -5824,6 +6395,9 @@ report reason */ /* No comment provided by engineer. */ "Use for messages" = "Für Nachrichten verwenden"; +/* No comment provided by engineer. */ +"Use for new channels" = "Für neue Kanäle verwenden"; + /* No comment provided by engineer. */ "Use for new connections" = "Für neue Verbindungen nutzen"; @@ -5848,6 +6422,9 @@ report reason */ /* No comment provided by engineer. */ "Use private routing with unknown servers." = "Sie nutzen privates Routing mit unbekannten Servern."; +/* No comment provided by engineer. */ +"Use relay" = "Relais verwenden"; + /* No comment provided by engineer. */ "Use server" = "Server nutzen"; @@ -5872,6 +6449,9 @@ report reason */ /* No comment provided by engineer. */ "Use the app with one hand." = "Die App mit einer Hand bedienen."; +/* No comment provided by engineer. */ +"Use this address in your social media profile, website, or email signature." = "Diese Adresse in Ihrem Social‑Media‑Profil, auf Ihrer Webseite oder in Ihrer E‑Mail‑Signatur verwenden."; + /* No comment provided by engineer. */ "Use web port" = "Web-Port nutzen"; @@ -5890,6 +6470,9 @@ report reason */ /* No comment provided by engineer. */ "v%@ (%@)" = "v%@ (%@)"; +/* relay test step */ +"Verify" = "Überprüfen"; + /* No comment provided by engineer. */ "Verify code with desktop" = "Code mit dem Desktop überprüfen"; @@ -5911,6 +6494,9 @@ report reason */ /* No comment provided by engineer. */ "Verify security code" = "Sicherheitscode überprüfen"; +/* relay hostname */ +"via %@" = "via %@"; + /* No comment provided by engineer. */ "Via browser" = "Über den Browser"; @@ -5980,9 +6566,18 @@ report reason */ /* No comment provided by engineer. */ "Voice messages prohibited!" = "Sprachnachrichten sind nicht erlaubt!"; +/* alert action */ +"Wait" = "Abwarten"; + +/* relay test step */ +"Wait response" = "Antwort abwarten"; + /* No comment provided by engineer. */ "waiting for answer…" = "Warten auf Antwort…"; +/* No comment provided by engineer. */ +"Waiting for channel owner to add relays." = "Warte auf das Hinzufügen von Relais durch den Eigentümer des Kanals."; + /* No comment provided by engineer. */ "waiting for confirmation…" = "Warten auf Bestätigung…"; @@ -6013,6 +6608,9 @@ report reason */ /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Warnung: Sie könnten einige Daten verlieren!"; +/* No comment provided by engineer. */ +"We made connecting simpler for new users." = "Wir haben das Verbinden für neue Nutzer vereinfacht."; + /* No comment provided by engineer. */ "WebRTC ICE servers" = "WebRTC ICE-Server"; @@ -6049,6 +6647,9 @@ report reason */ /* 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." = "Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil auch für die Gruppen verwendet, für die Sie von diesem Kontakt eingeladen werden."; +/* No comment provided by engineer. */ +"Why SimpleX is built." = "Warum SimpleX entwickelt wurde."; + /* No comment provided by engineer. */ "WiFi" = "WiFi"; @@ -6148,6 +6749,9 @@ report reason */ /* No comment provided by engineer. */ "you are observer" = "Sie sind Beobachter"; +/* No comment provided by engineer. */ +"you are subscriber" = "Sie sind Abonnent"; + /* snd group event chat item */ "you blocked %@" = "Sie haben %@ blockiert"; @@ -6190,6 +6794,9 @@ report reason */ /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Über die Geräte-Einstellungen können Sie die Benachrichtigungsvorschau im Sperrbildschirm erlauben."; +/* No comment provided by engineer. */ +"You can share a link or a QR code - anybody will be able to join the channel." = "Sie können einen Link oder QR-Code teilen - damit kann jeder dem Kanal beitreten."; + /* 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." = "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."; @@ -6230,10 +6837,13 @@ report reason */ "you changed role of %@ to %@" = "Sie haben die Rolle von %1$@ auf %2$@ geändert"; /* No comment provided by engineer. */ -"You could not be verified; please try again." = "Sie konnten nicht überprüft werden; bitte versuchen Sie es erneut."; +"You commit to:\n- Only legal content in public groups\n- Respect other users - no spam" = "Sie verpflichten sich dazu:\n- nur legale Inhalte in öffentlichen Gruppen zu versenden\n- andere Nutzer zu respektieren - kein Spam"; /* No comment provided by engineer. */ -"You decide who can connect." = "Sie entscheiden, wer sich mit Ihnen verbinden kann."; +"You connected to the channel via this relay link." = "Sie haben sich über diesen Relais‑Link mit dem Kanal verbunden."; + +/* No comment provided by engineer. */ +"You could not be verified; please try again." = "Sie konnten nicht überprüft werden; bitte versuchen Sie es erneut."; /* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Sie haben bereits ein Verbindungsanfrage beantragt!\nVerbindungsanfrage wiederholen?"; @@ -6289,6 +6899,9 @@ report reason */ /* snd group event chat item */ "you unblocked %@" = "Sie haben %@ freigegeben"; +/* No comment provided by engineer. */ +"You were born without an account" = "Sie wurden ohne eine Benutzerkennung geboren."; + /* No comment provided by engineer. */ "You will be able to send messages **only after your request is accepted**." = "Sie können erst dann Nachrichten versenden, **sobald Ihre Anfrage angenommen wurde**."; @@ -6310,6 +6923,9 @@ report reason */ /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind."; +/* No comment provided by engineer. */ +"You will stop receiving messages from this channel. Chat history will be preserved." = "Sie werden keine Nachrichten mehr aus diesem Kanal erhalten. Der Chatverlauf bleibt erhalten."; + /* No comment provided by engineer. */ "You will stop receiving messages from this chat. Chat history will be preserved." = "Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf bleibt erhalten."; @@ -6334,6 +6950,9 @@ report reason */ /* No comment provided by engineer. */ "Your calls" = "Anrufe"; +/* No comment provided by engineer. */ +"Your channel" = "Ihr Kanal"; + /* No comment provided by engineer. */ "Your chat database" = "Chat-Datenbank"; @@ -6364,6 +6983,9 @@ report reason */ /* No comment provided by engineer. */ "Your contacts will remain connected." = "Ihre Kontakte bleiben weiterhin verbunden."; +/* No comment provided by engineer. */ +"Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public." = "Ihre Kommunikation gehört Ihnen, so wie es immer war, bevor es das Internet gab. Das Netzwerk ist kein Ort, den Sie besuchen. Es ist ein Ort, den Sie erschaffen und besitzen und Niemand kann es Ihnen nehmen, egal ob Sie es privat oder öffentlich machen."; + /* No comment provided by engineer. */ "Your credentials may be sent unencrypted." = "Ihre Anmeldeinformationen können unverschlüsselt versendet werden."; @@ -6379,6 +7001,9 @@ report reason */ /* No comment provided by engineer. */ "Your ICE servers" = "Ihre ICE-Server"; +/* No comment provided by engineer. */ +"Your network" = "Ihr Netzwerk"; + /* No comment provided by engineer. */ "Your preferences" = "Ihre Präferenzen"; @@ -6388,6 +7013,9 @@ report reason */ /* No comment provided by engineer. */ "Your profile" = "Mein Profil"; +/* No comment provided by engineer. */ +"Your profile **%@** will be shared with channel relays and subscribers.\nRelays can access channel messages." = "Ihr Profil **%@** wird mit Kanal‑Relais und Abonnenten geteilt.\nRelais können auf Kanalnachrichten zugreifen."; + /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Ihr Profil **%@** wird geteilt."; @@ -6400,9 +7028,18 @@ report reason */ /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Ihr Profil wurde geändert. Wenn Sie es speichern, wird das aktualisierte Profil an alle Ihre Kontakte gesendet."; +/* No comment provided by engineer. */ +"Your public address" = "Ihre öffentliche Adresse"; + /* No comment provided by engineer. */ "Your random profile" = "Ihr Zufallsprofil"; +/* No comment provided by engineer. */ +"Your relay address" = "Ihre Relais-Adresse"; + +/* No comment provided by engineer. */ +"Your relay name" = "Ihr Relais-Name"; + /* No comment provided by engineer. */ "Your server address" = "Ihre Serveradresse"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index a05bc9f4b6..49826ff7f6 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -10,6 +10,9 @@ /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- entrega de mensajes más estable.\n- grupos un poco mejores.\n- ¡y más!"; +/* No comment provided by engineer. */ +"- opt-in to send link previews.\n- prevent hyperlink phishing.\n- remove link tracking." = "- aceptar el envío de vistas previas de los enlaces.\n- prevenir el phishing mediante hipervínculos.\n- eliminar el seguimiento de los enlaces."; + /* No comment provided by engineer. */ "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- notificar opcionalmente a los contactos eliminados.\n- nombres de perfil con espacios.\n- ¡...y más!"; @@ -19,21 +22,21 @@ /* No comment provided by engineer. */ "!1 colored!" = "!1 coloreado!"; +/* chat link info line */ +"(from owner)" = "(del propietario)"; + /* No comment provided by engineer. */ "(new)" = "(nuevo)"; +/* chat link info line */ +"(signed)" = "(firmado)"; + /* No comment provided by engineer. */ "(this device v%@)" = "(este dispositivo v%@)"; -/* No comment provided by engineer. */ -"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Contribuye](https://github.com/simplex-chat/simplex-chat#contribute)"; - /* No comment provided by engineer. */ "[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)"; - /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Añadir contacto**: crea un enlace de invitación nuevo."; @@ -64,6 +67,9 @@ /* No comment provided by engineer. */ "**Scan / Paste link**: to connect via a link you received." = "**Escanear / Pegar enlace**: para conectar mediante un enlace recibido."; +/* No comment provided by engineer. */ +"**Test relay** to retrieve its name." = "**Test servidor** para recibir su nombre."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Advertencia**: Las notificaciones automáticas instantáneas requieren una contraseña guardada en Keychain."; @@ -175,6 +181,18 @@ /* time interval */ "%d months" = "%d mes(es)"; +/* channel relay bar +channel subscriber relay bar */ +"%d relays failed" = "%d servidores han fallado"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays not active" = "%d servidores inactivos"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays removed" = "%d servidores eliminados"; + /* time interval */ "%d sec" = "%d segundo(s)"; @@ -184,15 +202,50 @@ /* integrity error chat item */ "%d skipped message(s)" = "%d mensaje(s) omitido(s)"; +/* channel subscriber count */ +"%d subscriber" = "%d suscriptor"; + +/* channel subscriber count */ +"%d subscribers" = "%d suscriptores"; + /* time interval */ "%d weeks" = "%d semana(s)"; +/* channel creation progress +channel relay bar progress */ +"%d/%d relays active" = "%1$d/%2$d servidores activos"; + +/* channel relay bar */ +"%d/%d relays active, %d errors" = "%1$d/%2$d servidores activos, %3$d errores"; + +/* channel creation progress with errors +channel relay bar */ +"%d/%d relays active, %d failed" = "%1$d/%2$d servidores activos, %3$d han fallado"; + +/* channel relay bar */ +"%d/%d relays active, %d removed" = "%1$d/%2$d servidores activos, %3$d servidores eliminados"; + +/* channel subscriber relay bar progress */ +"%d/%d relays connected" = "%1$d/%2$d servidores conectados"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d errors" = "%1$d/%2$d servidores conectados, %3$d errores"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d failed" = "%1$d/%2$d servidores conectados, %3$d con fallo"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d removed" = "%1$d/%2$d servidores conectados, %3$d eliminados"; + /* No comment provided by engineer. */ "%lld" = "%lld"; /* No comment provided by engineer. */ "%lld %@" = "%lld %@"; +/* No comment provided by engineer. */ +"%lld channel events" = "%lld eventos del canal"; + /* No comment provided by engineer. */ "%lld contact(s) selected" = "%lld contacto(s) seleccionado(s)"; @@ -200,7 +253,7 @@ "%lld file(s) with total size of %@" = "%lld archivo(s) con un tamaño total de %@"; /* No comment provided by engineer. */ -"%lld group events" = "%lld evento(s) de grupo"; +"%lld group events" = "%lld evento(s) del grupo"; /* No comment provided by engineer. */ "%lld members" = "%lld miembros"; @@ -262,6 +315,9 @@ /* No comment provided by engineer. */ "~strike~" = "\\~strike~"; +/* owner verification */ +"⚠️ Signature verification failed: %@." = "⚠️ Verificación de firma fallida: %@."; + /* time to disappear */ "0 sec" = "0 seg"; @@ -307,6 +363,9 @@ time interval */ /* No comment provided by engineer. */ "A few more things" = "Algunas cosas más"; +/* No comment provided by engineer. */ +"A link for one person to connect" = "Enlace para un solo contacto"; + /* notification title */ "A new contact" = "Contacto nuevo"; @@ -371,6 +430,9 @@ swipe action */ /* alert title */ "Accept member" = "Aceptar miembro"; +/* No comment provided by engineer. */ +"accepted" = "aceptado"; + /* rcv group event chat item */ "accepted %@" = "%@ aceptado"; @@ -392,6 +454,9 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledgement errors" = "Errores de confirmación"; +/* No comment provided by engineer. */ +"active" = "activo"; + /* token status text */ "Active" = "Activo"; @@ -399,7 +464,7 @@ swipe action */ "Active connections" = "Conexiones activas"; /* 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." = "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."; +"Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts." = "Añade la dirección a tu perfil para que tus contactos SimpleX puedan compartirla con otros. La actualización del perfil se enviará a tus contactos SimpleX."; /* No comment provided by engineer. */ "Add friends" = "Añadir amigos"; @@ -440,6 +505,9 @@ swipe action */ /* No comment provided by engineer. */ "Added message servers" = "Servidores de mensajes añadidos"; +/* No comment provided by engineer. */ +"Adding relays will be supported later." = "Añadir servidores estará disponible en una versión posterior."; + /* No comment provided by engineer. */ "Additional accent" = "Acento adicional"; @@ -530,6 +598,12 @@ swipe action */ /* profile dropdown */ "All profiles" = "Todos los perfiles"; +/* No comment provided by engineer. */ +"All relays failed" = "Todos los servidores han fallado"; + +/* No comment provided by engineer. */ +"All relays removed" = "Todos los servidores eliminados"; + /* No comment provided by engineer. */ "All reports will be archived for you." = "Todos los informes serán archivados para ti."; @@ -566,6 +640,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite. (24 horas)"; +/* No comment provided by engineer. */ +"Allow members to chat with admins." = "Permitir que los miembros chateen con administradores."; + /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "Se permiten las reacciones a los mensajes pero sólo si tu contacto también las permite."; @@ -575,12 +652,18 @@ swipe action */ /* No comment provided by engineer. */ "Allow sending direct messages to members." = "Se permiten mensajes directos entre miembros."; +/* No comment provided by engineer. */ +"Allow sending direct messages to subscribers." = "Se permiten mensajes directos entre suscriptores."; + /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Permites el envío de mensajes temporales."; /* No comment provided by engineer. */ "Allow sharing" = "Permitir compartir"; +/* No comment provided by engineer. */ +"Allow subscribers to chat with admins." = "Permitir que los suscriptores chateen con administradores."; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Se permite la eliminación irreversible de mensajes. (24 horas)"; @@ -650,9 +733,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Responder llamada"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "Cualquiera puede alojar servidores."; - /* No comment provided by engineer. */ "App build: %@" = "Compilación app: %@"; @@ -794,6 +874,15 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "ID de mensaje incorrecto"; +/* No comment provided by engineer. */ +"Be free\nin your network" = "Se libre\nen tu red"; + +/* No comment provided by engineer. */ +"Be free in your network." = "Se libre en tu red."; + +/* No comment provided by engineer. */ +"Because we destroyed the power to know who you are. So that your power can never be taken." = "Porque hemos destruido el poder de saber quien eres. De manera que tu poder nunca se pueda arrebatar."; + /* No comment provided by engineer. */ "Better calls" = "Llamadas mejoradas"; @@ -851,6 +940,9 @@ swipe action */ /* No comment provided by engineer. */ "Block member?" = "¿Bloquear miembro?"; +/* No comment provided by engineer. */ +"Block subscriber for all?" = "¿Bloquear al suscriptor para todos?"; + /* marked deleted chat item preview text */ "blocked" = "bloqueado"; @@ -895,9 +987,15 @@ marked deleted chat item preview text */ "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. */ -"Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Búlgaro, Finlandés, Tailandés y Ucraniano - gracias a los usuarios y [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +"Bottom bar" = "Barra inferior"; + +/* compose placeholder for channel owner */ +"Broadcast" = "Emisión"; /* 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)!" = "Búlgaro, Finlandés, Tailandés y Ucraniano - gracias a los usuarios y [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + +/* chat link info line */ "Business address" = "Dirección empresarial"; /* No comment provided by engineer. */ @@ -912,9 +1010,6 @@ marked deleted chat item preview text */ /* 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 (predeterminado) 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. */ -"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "Al usar SimpleX Chat, aceptas:\n- enviar únicamente contenido legal en los grupos públicos.\n- respetar a los demás usuarios – spam prohibido."; - /* No comment provided by engineer. */ "call" = "llamada"; @@ -939,6 +1034,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Camera not available" = "Cámara no disponible"; +/* No comment provided by engineer. */ +"can't broadcast" = "no puedes retransmitir"; + /* No comment provided by engineer. */ "Can't call contact" = "No se puede llamar al contacto"; @@ -1038,6 +1136,58 @@ set passcode view */ /* chat item text */ "changing address…" = "cambiando de servidor…"; +/* shown as sender role for channel messages */ +"channel" = "canal"; + +/* No comment provided by engineer. */ +"Channel" = "Canal"; + +/* No comment provided by engineer. */ +"Channel display name" = "Título mostrado del canal"; + +/* No comment provided by engineer. */ +"Channel full name (optional)" = "Título completo del canal (opcional)"; + +/* alert message +alert subtitle */ +"Channel has no active relays. Please try to join later." = "El canal no tiene servidores activos. Por favor, intenta unirte más tarde."; + +/* No comment provided by engineer. */ +"Channel image" = "Imagen del canal"; + +/* chat link info line */ +"Channel link" = "Enlace del canal"; + +/* No comment provided by engineer. */ +"Channel preferences" = "Preferencias del canal"; + +/* No comment provided by engineer. */ +"Channel profile" = "Perfil del canal"; + +/* No comment provided by engineer. */ +"Channel profile is stored on subscribers' devices and on the chat relays." = "El perfil del canal se almacena en los dispositivos de los suscriptores y en los servidores de chat."; + +/* snd group event chat item */ +"channel profile updated" = "perfil del canal actualizado"; + +/* alert message */ +"Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers." = "El perfil del canal ha sido modificado. Si lo guardas, el perfil actualizado será enviado a los suscriptores."; + +/* alert title */ +"Channel temporarily unavailable" = "Canales no disponibles temporalmente"; + +/* No comment provided by engineer. */ +"Channel will be deleted for all subscribers - this cannot be undone!" = "El canal será eliminado para todos los suscriptores. ¡No puede deshacerse!"; + +/* No comment provided by engineer. */ +"Channel will be deleted for you - this cannot be undone!" = "El canal será eliminado para tí. ¡No puede deshacerse!"; + +/* alert message */ +"Channel will start working with %d of %d relays. Proceed?" = "El canal comenzará a funcionar con %1$d de %2$d servidores. ¿Continuar?"; + +/* No comment provided by engineer. */ +"Channels" = "Canales"; + /* No comment provided by engineer. */ "Chat" = "Chat"; @@ -1089,6 +1239,18 @@ set passcode view */ /* No comment provided by engineer. */ "Chat profile" = "Perfil de usuario"; +/* No comment provided by engineer. */ +"Chat relay" = "Servidor de chat"; + +/* No comment provided by engineer. */ +"Chat relays" = "Servidores de chat"; + +/* No comment provided by engineer. */ +"Chat relays forward messages in channels you create." = "Los servidores de chat reenvían los mensajes en los canales que has creado."; + +/* No comment provided by engineer. */ +"Chat relays forward messages to channel subscribers." = "Los servidores de chat reenvían los mensajes a los suscriptores del canal."; + /* No comment provided by engineer. */ "Chat theme" = "Tema de chat"; @@ -1098,7 +1260,8 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "El chat será eliminado para tí. ¡No puede deshacerse!"; -/* chat toolbar */ +/* chat feature +chat toolbar */ "Chat with admins" = "Chatea con administradores"; /* No comment provided by engineer. */ @@ -1110,15 +1273,30 @@ set passcode view */ /* No comment provided by engineer. */ "Chats" = "Chats"; +/* No comment provided by engineer. */ +"Chats with admins are prohibited." = "Chat con administradores no permitido."; + +/* alert message */ +"Chats with admins in public channels have no E2E encryption - use only with trusted chat relays." = "El chat con administradores en el canal público no dispone de cifrado E2E. Úsalo sólo con servidores de confianza."; + /* No comment provided by engineer. */ "Chats with members" = "Chat con miembros"; +/* No comment provided by engineer. */ +"Chats with members are disabled" = "Chats con miembros desactivado"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Comprobar mensajes cada 20 min."; /* No comment provided by engineer. */ "Check messages when allowed." = "Comprobar mensajes cuando se permita."; +/* alert message */ +"Check relay address and try again." = "Comprueba la dirección del servidor y prueba de nuevo."; + +/* alert message */ +"Check relay name and try again." = "Comprueba el nombre del servidor y prueba de nuevo."; + /* alert title */ "Check server address and try again." = "Comprueba la dirección del servidor e inténtalo de nuevo."; @@ -1213,7 +1391,7 @@ set passcode view */ "Configure ICE servers" = "Configure servidores ICE"; /* No comment provided by engineer. */ -"Configure server operators" = "Configurar operadores de servidores"; +"Configure relays" = "Configurar servidores"; /* No comment provided by engineer. */ "Confirm" = "Confirmar"; @@ -1248,7 +1426,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Confirmado"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Conectar"; /* No comment provided by engineer. */ @@ -1278,6 +1457,9 @@ set passcode view */ /* new chat sheet title */ "Connect via link" = "Conectar mediante enlace"; +/* No comment provided by engineer. */ +"Connect via link or QR code" = "Conecta vía enlace o QR"; + /* new chat sheet title */ "Connect via one-time link" = "Conectar mediante enlace de un sólo uso"; @@ -1347,12 +1529,15 @@ set passcode view */ /* alert title */ "Connection error" = "Error conexión"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "Error de conexión (Autenticación)"; /* chat list item title (it should not be shown */ "connection established" = "conexión establecida"; +/* No comment provided by engineer. */ +"Connection failed" = "Conexión fallida"; + /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "Conexión bloqueada por el operador del servidor:\n%@"; @@ -1389,6 +1574,9 @@ set passcode view */ /* profile update event chat item */ "contact %@ changed to %@" = "el contacto %1$@ ha cambiado a %2$@"; +/* chat link info line */ +"Contact address" = "Dirección de contacto"; + /* No comment provided by engineer. */ "Contact allows" = "El contacto permite"; @@ -1449,6 +1637,9 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "Continuar"; +/* No comment provided by engineer. */ +"Contribute" = "Contribuye"; + /* No comment provided by engineer. */ "Conversation deleted!" = "¡Conversación eliminada!"; @@ -1464,12 +1655,9 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Esquina"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "¿Corregir el nombre a %@?"; -/* No comment provided by engineer. */ -"Create" = "Crear"; - /* No comment provided by engineer. */ "Create 1-time link" = "Crear enlace de un solo uso"; @@ -1497,6 +1685,12 @@ set passcode view */ /* No comment provided by engineer. */ "Create profile" = "Crear perfil"; +/* No comment provided by engineer. */ +"Create public channel" = "Crear canal público"; + +/* No comment provided by engineer. */ +"Create public channel (BETA)" = "Crear canal público (BETA)"; + /* server test step */ "Create queue" = "Crear cola"; @@ -1506,9 +1700,15 @@ set passcode view */ /* No comment provided by engineer. */ "Create your address" = "Crea tu dirección"; +/* No comment provided by engineer. */ +"Create your link" = "Crea tu enlace"; + /* No comment provided by engineer. */ "Create your profile" = "Crea tu perfil"; +/* No comment provided by engineer. */ +"Create your public address" = "Crea tu dirección pública"; + /* No comment provided by engineer. */ "Created" = "Creadas"; @@ -1521,6 +1721,9 @@ set passcode view */ /* No comment provided by engineer. */ "Creating archive link" = "Creando enlace al archivo"; +/* No comment provided by engineer. */ +"Creating channel" = "Creando canal"; + /* No comment provided by engineer. */ "Creating link…" = "Creando enlace…"; @@ -1623,8 +1826,8 @@ set passcode view */ /* No comment provided by engineer. */ "Debug delivery" = "Informe debug"; -/* No comment provided by engineer. */ -"Decentralized" = "Descentralizada"; +/* relay test step */ +"Decode link" = "Decodificar enlace"; /* message decrypt error item */ "Decryption error" = "Error descifrado"; @@ -1667,6 +1870,12 @@ swipe action */ /* No comment provided by engineer. */ "Delete and notify contact" = "Eliminar y notificar contacto"; +/* No comment provided by engineer. */ +"Delete channel" = "Eliminar canal"; + +/* No comment provided by engineer. */ +"Delete channel?" = "¿Eliminar el canal?"; + /* No comment provided by engineer. */ "Delete chat" = "Eliminar chat"; @@ -1770,6 +1979,9 @@ alert button */ /* server test step */ "Delete queue" = "Eliminar cola"; +/* No comment provided by engineer. */ +"Delete relay" = "Eliminar servidor"; + /* No comment provided by engineer. */ "Delete report" = "Eliminar informe"; @@ -1794,6 +2006,9 @@ alert button */ /* copied message info */ "Deleted at: %@" = "Eliminado: %@"; +/* rcv group event chat item */ +"deleted channel" = "canal eliminado"; + /* rcv direct event chat item */ "deleted contact" = "contacto eliminado"; @@ -1884,6 +2099,12 @@ alert button */ /* No comment provided by engineer. */ "Direct messages between members are prohibited." = "Los mensajes directos entre miembros del grupo no están permitidos."; +/* No comment provided by engineer. */ +"Direct messages between subscribers are prohibited." = "Los mensajes directos entre suscriptores del canal no están permitidos."; + +/* alert button */ +"Disable" = "Desactivar"; + /* No comment provided by engineer. */ "Disable (keep overrides)" = "Desactivar (conservando anulaciones)"; @@ -1891,7 +2112,7 @@ alert button */ "Disable automatic message deletion?" = "¿Desactivar la eliminación automática de mensajes?"; /* alert button */ -"Disable delete messages" = "Desactivar"; +"Disable delete messages" = "Desactivar eliminar mensajes"; /* No comment provided by engineer. */ "Disable for all" = "Desactivar para todos"; @@ -1941,6 +2162,9 @@ alert button */ /* No comment provided by engineer. */ "Do not send history to new members." = "No se envía el historial a los miembros nuevos."; +/* No comment provided by engineer. */ +"Do not send history to new subscribers." = "No se envía el historial a los suscriptores nuevos."; + /* No comment provided by engineer. */ "Do NOT send messages directly, even if your or destination server does not support private routing." = "NO enviar mensajes directamente incluso si tu servidor o el de destino no soportan enrutamiento privado."; @@ -2020,27 +2244,39 @@ chat item action */ /* No comment provided by engineer. */ "E2E encrypted notifications." = "Notificaciones cifradas E2E."; +/* No comment provided by engineer. */ +"Easier to invite your friends 👋" = "Invitar a tus amigos es más fácil 👋"; + /* chat item action */ "Edit" = "Editar"; +/* No comment provided by engineer. */ +"Edit channel profile" = "Editar perfil del canal"; + /* No comment provided by engineer. */ "Edit group profile" = "Editar perfil de grupo"; /* No comment provided by engineer. */ "Empty message!" = "¡Mensaje vacío!"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "Activar"; /* No comment provided by engineer. */ "Enable (keep overrides)" = "Activar (conservar anulaciones)"; +/* channel creation warning */ +"Enable at least one chat relay in Network & Servers." = "Activar al menos un servidor de chat en Servidores y Redes."; + /* alert title */ "Enable automatic message deletion?" = "¿Activar eliminación automática de mensajes?"; /* No comment provided by engineer. */ "Enable camera access" = "Permitir acceso a la cámara"; +/* alert title */ +"Enable chats with admins?" = "¿Activar chat con administradores?"; + /* No comment provided by engineer. */ "Enable disappearing messages by default." = "Activa por defecto los mensajes temporales."; @@ -2056,11 +2292,11 @@ chat item action */ /* No comment provided by engineer. */ "Enable instant notifications?" = "¿Activar notificaciones instantáneas?"; -/* No comment provided by engineer. */ -"Enable lock" = "Activar bloqueo"; +/* alert title */ +"Enable link previews?" = "¿Activar previsualización de enlaces?"; /* No comment provided by engineer. */ -"Enable notifications" = "Activar notificaciones"; +"Enable lock" = "Activar bloqueo"; /* No comment provided by engineer. */ "Enable periodic notifications?" = "¿Activar notificaciones periódicas?"; @@ -2167,6 +2403,9 @@ chat item action */ /* call status */ "ended call %@" = "llamada finalizada %@"; +/* No comment provided by engineer. */ +"Enter channel name…" = "Introduce el título del canal…"; + /* No comment provided by engineer. */ "Enter correct passphrase." = "Introduce la contraseña correcta."; @@ -2185,6 +2424,12 @@ chat item action */ /* No comment provided by engineer. */ "Enter password above to show!" = "¡Introduce la contraseña arriba para mostrar!"; +/* No comment provided by engineer. */ +"Enter profile name..." = "Introduce el nombre del perfil…"; + +/* No comment provided by engineer. */ +"Enter relay name…" = "Introduce el nombre del servidor…"; + /* No comment provided by engineer. */ "Enter server manually" = "Añadir manualmente"; @@ -2203,7 +2448,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "error"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "Error"; /* No comment provided by engineer. */ @@ -2221,6 +2466,9 @@ chat item action */ /* No comment provided by engineer. */ "Error adding member(s)" = "Error al añadir miembro(s)"; +/* alert title */ +"Error adding relay" = "Error al añadir el servidor"; + /* alert title */ "Error adding server" = "Error al añadir servidor"; @@ -2257,6 +2505,9 @@ chat item action */ /* No comment provided by engineer. */ "Error creating address" = "Error al crear dirección"; +/* alert title */ +"Error creating channel" = "Error al crear el canal"; + /* No comment provided by engineer. */ "Error creating group" = "Error al crear grupo"; @@ -2338,9 +2589,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Error al abrir chat"; -/* No comment provided by engineer. */ -"Error opening group" = "Error al abrir el grupo"; - /* alert title */ "Error receiving file" = "Error al recibir archivo"; @@ -2365,6 +2613,9 @@ chat item action */ /* No comment provided by engineer. */ "Error resetting statistics" = "Error al restablecer las estadísticas"; +/* No comment provided by engineer. */ +"Error saving channel profile" = "Error al guardar el perfil del canal"; + /* alert title */ "Error saving chat list" = "Error al guardar listas"; @@ -2407,6 +2658,9 @@ chat item action */ /* No comment provided by engineer. */ "Error setting delivery receipts!" = "¡Error al configurar confirmaciones de entrega!"; +/* alert title */ +"Error sharing channel" = "Error al compartir el canal"; + /* No comment provided by engineer. */ "Error starting chat" = "Error al iniciar Chat"; @@ -2449,12 +2703,16 @@ chat item action */ /* No comment provided by engineer. */ "Error: " = "Error: "; +/* receive error chat item */ +"error: %@" = "error: %@"; + /* alert message file error text snd error text */ "Error: %@" = "Error: %@"; -/* server test error */ +/* relay test error +server test error */ "Error: %@." = "Error: %@."; /* No comment provided by engineer. */ @@ -2502,6 +2760,9 @@ snd error text */ /* No comment provided by engineer. */ "Exporting database archive…" = "Exportando base de datos…"; +/* No comment provided by engineer. */ +"failed" = "fallo"; + /* No comment provided by engineer. */ "Failed to remove passphrase" = "Error al eliminar la contraseña"; @@ -2604,7 +2865,8 @@ snd error text */ /* No comment provided by engineer. */ "Fingerprint in server address does not match certificate: %@." = "La huella en la dirección del servidor no coincide con el certificado: %@."; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "La huella en la dirección del servidor no coincide con el certificado."; /* No comment provided by engineer. */ @@ -2628,7 +2890,11 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Para todos los moderadores"; -/* servers error */ +/* No comment provided by engineer. */ +"For anyone to reach you" = "Cualquiera puede contactarte"; + +/* servers error +servers warning */ "For chat profile %@:" = "Para el perfil de chat %@:"; /* No comment provided by engineer. */ @@ -2712,9 +2978,15 @@ snd error text */ /* No comment provided by engineer. */ "Further reduced battery usage" = "Reducción consumo de batería"; +/* relay test step */ +"Get link" = "Recibir el enlace"; + /* No comment provided by engineer. */ "Get notified when mentioned." = "Las menciones ahora se notifican."; +/* No comment provided by engineer. */ +"Get started" = "Empezar"; + /* No comment provided by engineer. */ "GIFs and stickers" = "GIFs y stickers"; @@ -2760,7 +3032,7 @@ snd error text */ /* No comment provided by engineer. */ "group is deleted" = "el grupo ha sido eliminado"; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "Enlace de grupo"; /* No comment provided by engineer. */ @@ -2832,6 +3104,9 @@ snd error text */ /* No comment provided by engineer. */ "History is not sent to new members." = "El historial no se envía a miembros nuevos."; +/* No comment provided by engineer. */ +"History is not sent to new subscribers." = "El historial no se envía a suscriptores nuevos."; + /* time unit */ "hours" = "horas"; @@ -2871,6 +3146,9 @@ snd error text */ /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "Si al abrir la aplicación introduces el código de autodestrucción:"; +/* down migration warning */ +"If you joined or created channels, they will stop working permanently." = "Si te has unido o has creado canales, dejarán de funcionar permanentemente."; + /* 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)." = "Si necesitas usar el chat ahora pulsa **Hacerlo más tarde** más abajo (se ofrecerá migrar la base de datos cuando se reinicie la aplicación)."; @@ -2889,9 +3167,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "Inmediatamente"; -/* No comment provided by engineer. */ -"Immune to spam" = "Inmune a spam y abuso"; - /* No comment provided by engineer. */ "Import" = "Importar"; @@ -2992,7 +3267,7 @@ snd error text */ "Initial role" = "Rol inicial"; /* No comment provided by engineer. */ -"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Instalar terminal para [SimpleX Chat](https://github.com/simplex-chat/simplex-chat)"; +"Install SimpleX Chat for terminal" = "Instalar terminal para SimpleX Chat"; /* No comment provided by engineer. */ "Instant" = "Al instante"; @@ -3027,7 +3302,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "datos Chat no válidos"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "Enlace de conexión no válido"; /* invalid chat item */ @@ -3042,12 +3317,18 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Confirmación de migración no válida"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "¡Nombre no válido!"; /* No comment provided by engineer. */ "Invalid QR code" = "Código QR no válido"; +/* alert title */ +"Invalid relay address!" = "¡Dirección de servidor no válido!"; + +/* alert title */ +"Invalid relay name!" = "¡Nombre de servidor no válido!"; + /* No comment provided by engineer. */ "Invalid response" = "Respuesta no válida"; @@ -3075,6 +3356,9 @@ snd error text */ /* No comment provided by engineer. */ "Invite members" = "Invitar miembros"; +/* No comment provided by engineer. */ +"Invite someone privately" = "Invitación privada"; + /* No comment provided by engineer. */ "Invite to chat" = "Invitar al chat"; @@ -3141,6 +3425,9 @@ snd error text */ /* No comment provided by engineer. */ "Join as %@" = "Unirme como %@"; +/* No comment provided by engineer. */ +"Join channel" = "Unirme al canal"; + /* new chat sheet title */ "Join group" = "Unirme al grupo"; @@ -3189,6 +3476,12 @@ snd error text */ /* swipe action */ "Leave" = "Salir"; +/* No comment provided by engineer. */ +"Leave channel" = "Salir del canal"; + +/* No comment provided by engineer. */ +"Leave channel?" = "¿Salir del canal?"; + /* No comment provided by engineer. */ "Leave chat" = "Salir del chat"; @@ -3207,6 +3500,9 @@ snd error text */ /* No comment provided by engineer. */ "Less traffic on mobile networks." = "Menos tráfico en redes móviles."; +/* No comment provided by engineer. */ +"Let someone connect to you" = "Conecta con alguien"; + /* email subject */ "Let's talk in SimpleX Chat" = "Hablemos en SimpleX Chat"; @@ -3216,9 +3512,15 @@ snd error text */ /* No comment provided by engineer. */ "Limitations" = "Limitaciones"; +/* No comment provided by engineer. */ +"link" = "enlace"; + /* No comment provided by engineer. */ "Link mobile and desktop apps! 🔗" = "¡Enlazar aplicación móvil con ordenador! 🔗"; +/* owner verification */ +"Link signature verified." = "Firma del enlace verificada."; + /* No comment provided by engineer. */ "Linked desktop options" = "Opciones ordenador enlazado"; @@ -3348,6 +3650,9 @@ snd error text */ /* No comment provided by engineer. */ "Members can add message reactions." = "Los miembros pueden añadir reacciones a los mensajes."; +/* No comment provided by engineer. */ +"Members can chat with admins." = "Los miembros pueden chatear con los administradores."; + /* No comment provided by engineer. */ "Members can irreversibly delete sent messages. (24 hours)" = "Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas)"; @@ -3390,6 +3695,9 @@ snd error text */ /* No comment provided by engineer. */ "Message draft" = "Borrador de mensaje"; +/* No comment provided by engineer. */ +"Message error" = "Mensaje de error"; + /* item status text */ "Message forwarded" = "Mensaje reenviado"; @@ -3450,6 +3758,12 @@ snd error text */ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "¡Los mensajes nuevos de %@ serán mostrados!"; +/* No comment provided by engineer. */ +"Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages." = "Los mensajes en este canal **no están cifrados de extremo a extremo**. Los servidores pueden ver estos mensajes."; + +/* E2EE info chat item */ +"Messages in this channel are not end-to-end encrypted. Chat relays can see these messages." = "Los mensajes en este canal no están cifrados de extremo a extremo. Los servidores pueden ver estos mensajes."; + /* alert message */ "Messages in this chat will never be deleted." = "Los mensajes de esta conversación nunca se eliminan."; @@ -3469,10 +3783,10 @@ snd error text */ "Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Los mensajes, archivos y llamadas están protegidos mediante **cifrado de extremo a extremo resistente a tecnología cuántica** con secreto perfecto hacía adelante, repudio y recuperación tras ataque."; /* No comment provided by engineer. */ -"Migrate device" = "Migrar dispositivo"; +"Migrate" = "Migrar"; /* No comment provided by engineer. */ -"Migrate from another device" = "Migrar desde otro dispositivo"; +"Migrate device" = "Migrar dispositivo"; /* No comment provided by engineer. */ "Migrate here" = "Migrar aquí"; @@ -3564,12 +3878,18 @@ snd error text */ /* No comment provided by engineer. */ "Network & servers" = "Servidores y Redes"; +/* No comment provided by engineer. */ +"Network commitments" = "Compromisos en la red"; + /* No comment provided by engineer. */ "Network connection" = "Conexión de red"; /* No comment provided by engineer. */ "Network decentralization" = "Descentralización de la red"; +/* conn error description */ +"Network error" = "Error de red"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "Problema en la red - el mensaje ha expirado tras muchos intentos de envío."; @@ -3579,6 +3899,9 @@ snd error text */ /* No comment provided by engineer. */ "Network operator" = "Operador de red"; +/* No comment provided by engineer. */ +"Network routers cannot know\nwho talks to whom" = "Los routers de la red no pueden saber\nquién se comunica con quién"; + /* No comment provided by engineer. */ "Network settings" = "Configuración de red"; @@ -3588,15 +3911,24 @@ snd error text */ /* delete after time */ "never" = "nunca"; +/* No comment provided by engineer. */ +"new" = "nuevo"; + /* token status text */ "New" = "Nuevo"; +/* No comment provided by engineer. */ +"New 1-time link" = "Nuevo enlace de 1 solo uso"; + /* No comment provided by engineer. */ "New chat" = "Nuevo chat"; /* No comment provided by engineer. */ "New chat experience 🎉" = "Nueva experiencia de chat 🎉"; +/* No comment provided by engineer. */ +"New chat relay" = "Nuevo servidor de chat"; + /* notification */ "New contact request" = "Nueva solicitud de contacto"; @@ -3654,9 +3986,21 @@ snd error text */ /* No comment provided by engineer. */ "No" = "No"; +/* No comment provided by engineer. */ +"No account. No phone. No email. No ID.\nThe most secure encryption." = "Sin cuenta. Sin teléfono. Sin email. Sin ID.\nEl cifrado más seguro."; + +/* No comment provided by engineer. */ +"No active relays" = "Sin servidores activos"; + /* Authentication unavailable */ "No app password" = "Sin contraseña de la aplicación"; +/* No comment provided by engineer. */ +"No chat relays" = "Sin servidores de chat"; + +/* servers warning */ +"No chat relays enabled." = "Ningún servidor de chat activado."; + /* No comment provided by engineer. */ "No chats" = "Sin chats"; @@ -3754,7 +4098,16 @@ snd error text */ "No unread chats" = "Ningún chat sin leer"; /* No comment provided by engineer. */ -"No user identifiers." = "Sin identificadores de usuario."; +"Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "Nadie monitorizaba tus conversaciones. Nadie registraba tus ubicaciones. La privacidad nunca fue un lujo, era la manera de vivir."; + +/* No comment provided by engineer. */ +"Non-profit governance" = "Gobernanza no lucrativa"; + +/* No comment provided by engineer. */ +"Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "No un candado mejorado en la puerta de otro. No un terrateniente que respeta tu privacidad pero sigue guardando un registro de tus visitantes. Tu no eres el invitado. Estás en tu casa y ningún rey podrá entrar. Tu eres el soberano."; + +/* alert title */ +"Not all relays connected" = "Hay servidores no conectados"; /* No comment provided by engineer. */ "Not compatible!" = "¡No compatible!"; @@ -3812,7 +4165,7 @@ alert button new chat action */ "Ok" = "Ok"; -/* No comment provided by engineer. */ +/* alert button */ "OK" = "OK"; /* No comment provided by engineer. */ @@ -3821,9 +4174,15 @@ new chat action */ /* group pref value */ "on" = "Activado"; +/* No comment provided by engineer. */ +"On your phone, not on servers." = "En tu teléfono, no en algún servidor."; + /* No comment provided by engineer. */ "One-time invitation link" = "Enlace de invitación de un solo uso"; +/* chat link info line */ +"One-time link" = "Enlace de un solo uso"; + /* No comment provided by engineer. */ "Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Se **requieren** hosts .onion para la conexión.\nRequiere activación de la VPN."; @@ -3833,6 +4192,9 @@ new chat action */ /* No comment provided by engineer. */ "Onion hosts will not be used." = "No se usarán hosts .onion."; +/* No comment provided by engineer. */ +"Only channel owners can change channel preferences." = "Sólo los propietarios pueden modificar las preferencias de los canales."; + /* No comment provided by engineer. */ "Only chat owners can change preferences." = "Sólo los propietarios del chat pueden cambiar las preferencias."; @@ -3893,12 +4255,16 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Sólo tu contacto puede enviar mensajes de voz."; -/* alert action */ +/* alert action +alert button */ "Open" = "Abrir"; /* No comment provided by engineer. */ "Open changes" = "Abrir cambios"; +/* new chat action */ +"Open channel" = "Abrir canal"; + /* new chat action */ "Open chat" = "Abrir chat"; @@ -3911,6 +4277,9 @@ new chat action */ /* No comment provided by engineer. */ "Open conditions" = "Abrir condiciones"; +/* alert title */ +"Open external link?" = "¿Abrir enlace externo?"; + /* alert action */ "Open full link" = "Abrir enlace completo"; @@ -3923,6 +4292,9 @@ new chat action */ /* authentication reason */ "Open migration to another device" = "Abrir menú migración a otro dispositivo"; +/* new chat action */ +"Open new channel" = "Abrir canal nuevo"; + /* new chat action */ "Open new chat" = "Abrir chat nuevo"; @@ -3953,6 +4325,9 @@ new chat action */ /* alert title */ "Operator server" = "Servidor del operador"; +/* No comment provided by engineer. */ +"Operators commit to:\n- Be independent\n- Minimize metadata usage\n- Run verified open-source code" = "Los operadores se comprometen a:\n- Ser independientes\n- Minimizar el tratamiento de metadatos\n- Ejecutar código open-source verificado"; + /* No comment provided by engineer. */ "Or import archive file" = "O importa desde un archivo"; @@ -3965,12 +4340,18 @@ new chat action */ /* No comment provided by engineer. */ "Or securely share this file link" = "O comparte de forma segura este enlace al archivo"; +/* No comment provided by engineer. */ +"Or show QR in person or via video call." = "O muestra el código QR en persona o por videollamada."; + /* No comment provided by engineer. */ "Or show this code" = "O muestra este código"; /* No comment provided by engineer. */ "Or to share privately" = "O para compartir en privado"; +/* No comment provided by engineer. */ +"Or use this QR - print or show online." = "O usa el QR, imprímelo o muestralo en línea."; + /* No comment provided by engineer. */ "Organize chats into lists" = "Organiza tus chats en listas"; @@ -3989,9 +4370,18 @@ new chat action */ /* member role */ "owner" = "propietario"; +/* No comment provided by engineer. */ +"Owner" = "Propietario"; + /* feature role */ "owners" = "propietarios"; +/* No comment provided by engineer. */ +"Owners" = "Propietarios"; + +/* No comment provided by engineer. */ +"Ownership: you can run your own relays." = "En propiedad: puedes poner en marcha tus propios servidores."; + /* No comment provided by engineer. */ "Passcode" = "Código de acceso"; @@ -4019,6 +4409,9 @@ new chat action */ /* No comment provided by engineer. */ "Paste image" = "Pegar imagen"; +/* No comment provided by engineer. */ +"Paste link / Scan" = "Pegar enlace / Escanear"; + /* No comment provided by engineer. */ "Paste link to connect!" = "Pegar enlace para conectar!"; @@ -4127,6 +4520,12 @@ new chat action */ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserva el último borrador del mensaje con los datos adjuntos."; +/* No comment provided by engineer. */ +"Preset relay address" = "Direcciones predefinidas"; + +/* No comment provided by engineer. */ +"Preset relay name" = "Nombres predefinidos"; + /* No comment provided by engineer. */ "Preset server address" = "Dirección predefinida del servidor"; @@ -4149,10 +4548,10 @@ new chat action */ "Privacy policy and conditions of use." = "Política de privacidad y condiciones de uso."; /* No comment provided by engineer. */ -"Privacy redefined" = "Privacidad redefinida"; +"Privacy: for owners and subscribers." = "Privacidad: para propietarios y suscriptores."; /* No comment provided by engineer. */ -"Private chats, groups and your contacts are not accessible to server operators." = "Los chats privados, los grupos y tus contactos no son accesibles para los operadores de servidores."; +"Private and secure messaging." = "Mensajería segura y privada."; /* No comment provided by engineer. */ "Private filenames" = "Nombres de archivos privados"; @@ -4178,6 +4577,9 @@ new chat action */ /* alert title */ "Private routing timeout" = "Timeout enrutamiento privado"; +/* alert action */ +"Proceed" = "Continuar"; + /* No comment provided by engineer. */ "Profile and server connections" = "Eliminar perfil y conexiones"; @@ -4194,11 +4596,14 @@ new chat action */ "Profile theme" = "Tema del perfil"; /* alert message */ -"Profile update will be sent to your contacts." = "La actualización del perfil se enviará a tus contactos."; +"Profile update will be sent to your SimpleX contacts." = "La actualización del perfil se enviará a tus contactos SimpleX."; /* No comment provided by engineer. */ "Prohibit audio/video calls." = "No se permiten llamadas y videollamadas."; +/* No comment provided by engineer. */ +"Prohibit chats with admins." = "El chat con los administradores no está permitido."; + /* No comment provided by engineer. */ "Prohibit irreversible message deletion." = "No se permite la eliminación irreversible de mensajes."; @@ -4214,6 +4619,9 @@ new chat action */ /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "No se permiten mensajes directos entre miembros."; +/* No comment provided by engineer. */ +"Prohibit sending direct messages to subscribers." = "No se permiten mensajes directos entre suscriptores."; + /* No comment provided by engineer. */ "Prohibit sending disappearing messages." = "No se permiten mensajes temporales."; @@ -4256,6 +4664,9 @@ new chat action */ /* No comment provided by engineer. */ "Proxy requires password" = "El proxy requiere contraseña"; +/* No comment provided by engineer. */ +"Public channels - speak freely 🚀" = "Canales públicos - habla con libertad 🚀"; + /* No comment provided by engineer. */ "Push notifications" = "Notificaciones push"; @@ -4284,16 +4695,10 @@ new chat action */ "Read more" = "Saber más"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Conoce más en la [Guía del Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in our GitHub repository." = "Conoce más en nuestro repositorio GitHub."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; - -/* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Conoce más en el [Manual del Usuario](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)." = "Conoce más en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme)."; +"Read more in User Guide." = "Conoce más en la Guía del Usuario."; /* No comment provided by engineer. */ "Receipts are disabled" = "Las confirmaciones están desactivadas"; @@ -4402,12 +4807,36 @@ swipe action */ /* call status */ "rejected call" = "llamada rechazada"; +/* member role */ +"relay" = "servidor"; + +/* No comment provided by engineer. */ +"Relay" = "Servidor"; + +/* alert title */ +"Relay address" = "Dirección del servidor"; + +/* alert title */ +"Relay connection failed" = "La conexión con el servidor ha fallado"; + +/* No comment provided by engineer. */ +"Relay link" = "Enlace servidor"; + +/* alert message */ +"Relay results:" = "Resultados del servidor:"; + /* 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 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 IP pero puede ver la duración de la llamada."; +/* No comment provided by engineer. */ +"Relay test failed!" = "¡El test del servidor ha fallado!"; + +/* No comment provided by engineer. */ +"Reliability: many relays per channel." = "Fiabilidad: muchos servidores por canal."; + /* alert action */ "Remove" = "Eliminar"; @@ -4432,12 +4861,24 @@ swipe action */ /* No comment provided by engineer. */ "Remove passphrase from keychain?" = "¿Eliminar contraseña de Keychain?"; +/* No comment provided by engineer. */ +"Remove subscriber" = "Eliminar suscriptor"; + +/* alert title */ +"Remove subscriber?" = "¿Eliminar suscriptor?"; + /* No comment provided by engineer. */ "removed" = "expulsado"; +/* receive error chat item */ +"removed (%d attempts)" = "eliminado (%d intentos)"; + /* rcv group event chat item */ "removed %@" = "ha expulsado a %@"; +/* No comment provided by engineer. */ +"removed by operator" = "eliminado por el operador"; + /* profile update event chat item */ "removed contact address" = "dirección de contacto eliminada"; @@ -4606,6 +5047,9 @@ swipe action */ /* No comment provided by engineer. */ "Run chat" = "Ejecutar SimpleX"; +/* No comment provided by engineer. */ +"Safe web links" = "Enlaces web seguros"; + /* No comment provided by engineer. */ "Safely receive files" = "Recibe archivos de forma segura"; @@ -4622,6 +5066,9 @@ chat item action */ /* alert button */ "Save (and notify members)" = "Guardar (y notificar miembros)"; +/* alert button */ +"Save (and notify subscribers)" = "Guardar (y notificar suscriptores)"; + /* alert title */ "Save admission settings?" = "¿Guardar configuración?"; @@ -4631,12 +5078,21 @@ chat item action */ /* No comment provided by engineer. */ "Save and notify group members" = "Guardar y notificar grupo"; +/* No comment provided by engineer. */ +"Save and notify subscribers" = "Guardar y notificar suscriptores"; + /* No comment provided by engineer. */ "Save and reconnect" = "Guardar y reconectar"; /* No comment provided by engineer. */ "Save and update group profile" = "Guardar y actualizar perfil del grupo"; +/* No comment provided by engineer. */ +"Save channel profile" = "Guardar perfil del canal"; + +/* alert title */ +"Save channel profile?" = "¿Guardar perfil del canal?"; + /* No comment provided by engineer. */ "Save group profile" = "Guardar perfil de grupo"; @@ -4766,6 +5222,9 @@ chat item action */ /* chat item text */ "security code changed" = "código de seguridad cambiado"; +/* No comment provided by engineer. */ +"Security: owners hold channel keys." = "Seguridad: los propietarios tienen la llave del canal."; + /* chat item action */ "Select" = "Seleccionar"; @@ -4844,12 +5303,18 @@ chat item action */ /* No comment provided by engineer. */ "Send request without message" = "Enviar solicitud sin mensaje"; +/* No comment provided by engineer. */ +"Send the link via any messenger - it's secure. Ask to paste into SimpleX." = "Envía el enlace con cualquier mensajero, es seguro. El contacto debe pegarlo en SimpleX."; + /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Envíalos desde la galería o desde teclados personalizados."; /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Se envían hasta 100 mensajes más recientes a los miembros nuevos."; +/* No comment provided by engineer. */ +"Send up to 100 last messages to new subscribers." = "Se envían hasta 100 mensajes más recientes a los suscriptores nuevos."; + /* No comment provided by engineer. */ "Send your private feedback to groups." = "Envía tu comentario privado a los grupos."; @@ -4859,6 +5324,9 @@ chat item action */ /* No comment provided by engineer. */ "Sender may have deleted the connection request." = "El remitente puede haber eliminado la solicitud de conexión."; +/* alert message */ +"Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later." = "Enviar una previsualización del enlace puede revelar tu dirección IP al sitio web. Puedes cambiarlo más tarde en los ajustes de privacidad."; + /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "El envío de confirmaciones de entrega se activará para todos los contactos en todos los perfiles visibles."; @@ -4937,6 +5405,9 @@ chat item action */ /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "información cola del servidor: %1$@\n\núltimo mensaje recibido: %2$@"; +/* relay test error */ +"Server requires authorization to connect to relay, check password." = "El servidor requiere autorización para conectar con el servidor, comprueba la contraseña."; + /* server test error */ "Server requires authorization to create queues, check password." = "El servidor requiere autorización para crear colas, comprueba la contraseña."; @@ -5021,6 +5492,12 @@ chat item action */ /* alert message */ "Settings were changed." = "La configuración ha sido modificada."; +/* No comment provided by engineer. */ +"Setup notifications" = "Configurar notificaciones"; + +/* No comment provided by engineer. */ +"Setup routers" = "Configurar routers"; + /* No comment provided by engineer. */ "Shape profile images" = "Dar forma a las imágenes de perfil"; @@ -5041,7 +5518,10 @@ chat item action */ "Share address publicly" = "Campartir dirección públicamente"; /* alert title */ -"Share address with contacts?" = "¿Compartir la dirección con los contactos?"; +"Share address with SimpleX contacts?" = "¿Compartir la dirección con los contactos SimpleX?"; + +/* No comment provided by engineer. */ +"Share channel" = "Compartir canal"; /* No comment provided by engineer. */ "Share from other apps." = "Comparte desde otras aplicaciones."; @@ -5058,6 +5538,9 @@ chat item action */ /* No comment provided by engineer. */ "Share profile" = "Perfil a compartir"; +/* No comment provided by engineer. */ +"Share relay address" = "Compartir dirección del servidor"; + /* No comment provided by engineer. */ "Share SimpleX address on social media." = "Comparte tu dirección SimpleX en redes sociales."; @@ -5068,7 +5551,10 @@ chat item action */ "Share to SimpleX" = "Compartir con Simplex"; /* No comment provided by engineer. */ -"Share with contacts" = "Compartir con contactos"; +"Share via chat" = "Compartir mediante chat"; + +/* No comment provided by engineer. */ +"Share with SimpleX contacts" = "Compartir con contactos SimpleX"; /* No comment provided by engineer. */ "Share your address" = "Comparte tu dirección"; @@ -5173,7 +5659,7 @@ chat item action */ "SimpleX protocols reviewed by Trail of Bits." = "Protocolos de SimpleX auditados por Trail of Bits."; /* simplex link type */ -"SimpleX relay link" = "Enlace de servidor SimpleX"; +"SimpleX relay address" = "Dirección de servidor SimpleX"; /* No comment provided by engineer. */ "Simplified incognito mode" = "Modo incógnito simplificado"; @@ -5227,6 +5713,9 @@ report reason */ /* chat item text */ "standard end-to-end encryption" = "cifrado estándar de extremo a extremo"; +/* No comment provided by engineer. */ +"Star on GitHub" = "Estrella en GitHub"; + /* No comment provided by engineer. */ "Start chat" = "Iniciar chat"; @@ -5293,6 +5782,48 @@ report reason */ /* No comment provided by engineer. */ "Subscribed" = "Suscritas"; +/* No comment provided by engineer. */ +"Subscriber" = "Suscriptor"; + +/* chat feature */ +"Subscriber reports" = "Informes de suscriptores"; + +/* alert message */ +"Subscriber will be removed from channel - this cannot be undone!" = "El suscriptor será eliminado del canal. ¡No puede deshacerse!"; + +/* No comment provided by engineer. */ +"Subscribers" = "Suscriptores"; + +/* No comment provided by engineer. */ +"Subscribers can add message reactions." = "Los suscriptores pueden añadir reacciones a los mensajes."; + +/* No comment provided by engineer. */ +"Subscribers can chat with admins." = "Los suscriptores pueden chatear con los administradores."; + +/* No comment provided by engineer. */ +"Subscribers can irreversibly delete sent messages. (24 hours)" = "Los suscriptores del canal pueden eliminar mensajes de forma irreversible. (24 horas)"; + +/* No comment provided by engineer. */ +"Subscribers can report messsages to moderators." = "Los suscriptores pueden informar de mensajes a los moderadores."; + +/* No comment provided by engineer. */ +"Subscribers can send direct messages." = "Los suscriptores del canal pueden enviar mensajes directos."; + +/* No comment provided by engineer. */ +"Subscribers can send disappearing messages." = "Los suscriptores del canal pueden enviar mensajes temporales."; + +/* No comment provided by engineer. */ +"Subscribers can send files and media." = "Los suscriptores del canal pueden enviar archivos y multimedia."; + +/* No comment provided by engineer. */ +"Subscribers can send SimpleX links." = "Los suscriptores del canal pueden enviar enlaces SimpleX."; + +/* No comment provided by engineer. */ +"Subscribers can send voice messages." = "Los suscriptores del canal pueden enviar mensajes de voz."; + +/* No comment provided by engineer. */ +"Subscribers use relay link to connect to the channel.\nRelay address was used to set up this relay for the channel." = "Los suscriptores usan el enlace del servidor para conectarse a los canales.\nLa dirección del servidor se usó para establecer el servidor para el canal."; + /* No comment provided by engineer. */ "Subscription errors" = "Errores de suscripción"; @@ -5320,6 +5851,9 @@ report reason */ /* No comment provided by engineer. */ "Take picture" = "Hacer foto"; +/* No comment provided by engineer. */ +"Talk to someone" = "Para comunicarte"; + /* No comment provided by engineer. */ "Tap button " = "Pulsa el botón "; @@ -5333,7 +5867,7 @@ report reason */ "Tap Connect to use bot" = "Pulsa Conectar para usar el bot"; /* No comment provided by engineer. */ -"Tap Create SimpleX address in the menu to create it later." = "Pulsa Crear dirección SimpleX en el menú para crearla más tarde."; +"Tap Join channel" = "Pulsa Unirme al canal"; /* No comment provided by engineer. */ "Tap Join group" = "Pulsa Unirme al grupo"; @@ -5350,6 +5884,9 @@ report reason */ /* No comment provided by engineer. */ "Tap to join incognito" = "Pulsa para unirte en modo incógnito"; +/* No comment provided by engineer. */ +"Tap to open" = "Pulsa para abrir"; + /* No comment provided by engineer. */ "Tap to paste link" = "Pulsa aquí para pegar el enlace"; @@ -5380,12 +5917,16 @@ report reason */ /* file error alert title */ "Temporary file error" = "Error en archivo temporal"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Prueba no superada en el paso %@."; /* No comment provided by engineer. */ "Test notifications" = "Probar notificaciones"; +/* No comment provided by engineer. */ +"Test relay" = "Test servidor"; + /* No comment provided by engineer. */ "Test server" = "Probar servidor"; @@ -5413,6 +5954,9 @@ report reason */ /* No comment provided by engineer. */ "The app protects your privacy by using different operators in each conversation." = "La aplicación protege tu privacidad mediante el uso de diferentes operadores en cada conversación."; +/* No comment provided by engineer. */ +"The app removed this message after %lld attempts to receive it." = "La app ha eliminado el mensaje tras %lld intentos de recibirlo."; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "La aplicación pedirá que confirmes las descargas desde servidores de archivos desconocidos (excepto si son .onion)."; @@ -5422,6 +5966,9 @@ report reason */ /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "El código QR escaneado no es un enlace de SimpleX."; +/* conn error description */ +"The connection reached the limit of undelivered messages" = "La conexión ha alcanzado al límite de mensajes no entregados"; + /* No comment provided by engineer. */ "The connection reached the limit of undelivered messages, your contact may be offline." = "La conexión ha alcanzado el límite de mensajes no entregados. es posible que tu contacto esté desconectado."; @@ -5438,7 +5985,7 @@ report reason */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "El cifrado funciona y un cifrado nuevo no es necesario. ¡Podría dar lugar a errores de conexión!"; /* No comment provided by engineer. */ -"The future of messaging" = "La nueva generación de mensajería privada"; +"The first network where you own\nyour contacts and groups." = "La primera red donde los grupos\ny los contactos son tuyos."; /* No comment provided by engineer. */ "The hash of the previous message is different." = "El hash del mensaje anterior es diferente."; @@ -5464,6 +6011,9 @@ report reason */ /* 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."; +/* No comment provided by engineer. */ +"The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it." = "La libertad más antigua del ser humano, la de hablar con otra persona sin ser observado, materializada sobre una infraestructura que no puede traicionarla."; + /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Las mismas condiciones se aplicarán al operador **%@**."; @@ -5491,6 +6041,12 @@ report reason */ /* No comment provided by engineer. */ "Themes" = "Temas"; +/* No comment provided by engineer. */ +"Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible." = "Después pasamos a internet y cada plataforma pedía una parte de tí: tu nombre, tu número, tus amistades. Aceptamos que el precio de hablar con los demás es informar a alguien de quién es interlocutor. Cada generación, personas y tecnología, ha funcionado así: teléfono, email, mensajería, redes sociales. Parecía el único camino."; + +/* No comment provided by engineer. */ +"There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected." = "Existe otro camino. Una red sin números de teléfono. Sin nombres de usuario. Sin cuentas. Sin identificadores de ningún tipo. Una red que conecta las personas y entrega mensajes cifrados sin saber quien está conectado."; + /* No comment provided by engineer. */ "These conditions will also apply for: **%@**." = "Estas condiciones también se aplican para: **%@**."; @@ -5533,6 +6089,12 @@ report reason */ /* No comment provided by engineer. */ "This group no longer exists." = "Este grupo ya no existe."; +/* alert message */ +"This is a chat relay address, it cannot be used to connect." = "Esto es una dirección de servidor, no puede usarse para conectar."; + +/* new chat action */ +"This is your link for channel %@!" = "Este es tu enlace para el canal %@!"; + /* No comment provided by engineer. */ "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Este enlace requiere una versión más reciente de la aplicación. Por favor, actualiza la aplicación o pide a tu contacto un enlace compatible."; @@ -5566,6 +6128,9 @@ report reason */ /* No comment provided by engineer. */ "To make a new connection" = "Para hacer una conexión nueva"; +/* No comment provided by engineer. */ +"To make SimpleX Network last." = "Para que la Red SimpleX perdure."; + /* No comment provided by engineer. */ "To protect against your link being replaced, you can compare contact security codes." = "Para protegerte contra una sustitución del enlace, puedes comparar los códigos de seguridad con tu contacto."; @@ -5614,9 +6179,6 @@ report reason */ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Para verificar el cifrado de extremo a extremo con tu contacto, compara (o escanea) el código en ambos dispositivos."; -/* No comment provided by engineer. */ -"Toggle chat list:" = "Alternar lista de chats:"; - /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Activa incógnito al conectar."; @@ -5626,6 +6188,9 @@ report reason */ /* No comment provided by engineer. */ "Toolbar opacity" = "Opacidad barra"; +/* No comment provided by engineer. */ +"Top bar" = "Barra superior"; + /* No comment provided by engineer. */ "Total" = "Total"; @@ -5665,6 +6230,9 @@ report reason */ /* No comment provided by engineer. */ "Unblock member?" = "¿Desbloquear miembro?"; +/* No comment provided by engineer. */ +"Unblock subscriber for all?" = "¿Desbloquear al suscriptor para todos?"; + /* rcv group event chat item */ "unblocked %@" = "ha desbloqueado a %@"; @@ -5737,12 +6305,15 @@ report reason */ /* swipe action */ "Unread" = "No leído"; -/* No comment provided by engineer. */ +/* conn error description */ "Unsupported connection link" = "Enlace de conexión no compatible"; /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Hasta 100 últimos mensajes son enviados a los miembros nuevos."; +/* No comment provided by engineer. */ +"Up to 100 last messages are sent to new subscribers." = "Hasta 100 últimos mensajes son enviados a los suscriptores nuevos."; + /* No comment provided by engineer. */ "Update" = "Actualizar"; @@ -5755,6 +6326,9 @@ report reason */ /* No comment provided by engineer. */ "Update settings?" = "¿Actualizar configuración?"; +/* rcv group event chat item */ +"updated channel profile" = "perfil del canal actualizado"; + /* No comment provided by engineer. */ "Updated conditions" = "Condiciones actualizadas"; @@ -5812,9 +6386,6 @@ report reason */ /* No comment provided by engineer. */ "Use %@" = "Usar %@"; -/* No comment provided by engineer. */ -"Use chat" = "Usar Chat"; - /* new chat action */ "Use current profile" = "Usar perfil actual"; @@ -5824,6 +6395,9 @@ report reason */ /* No comment provided by engineer. */ "Use for messages" = "Uso para mensajes"; +/* No comment provided by engineer. */ +"Use for new channels" = "Usar para canales nuevos"; + /* No comment provided by engineer. */ "Use for new connections" = "Para conexiones nuevas"; @@ -5848,6 +6422,9 @@ report reason */ /* No comment provided by engineer. */ "Use private routing with unknown servers." = "Usar enrutamiento privado con servidores de mensaje desconocidos."; +/* No comment provided by engineer. */ +"Use relay" = "Usar servidor"; + /* No comment provided by engineer. */ "Use server" = "Usar servidor"; @@ -5872,6 +6449,9 @@ report reason */ /* No comment provided by engineer. */ "Use the app with one hand." = "Usa la aplicación con una sola mano."; +/* No comment provided by engineer. */ +"Use this address in your social media profile, website, or email signature." = "Usa esta dirección en tu perfil de redes sociales, página web o firma email."; + /* No comment provided by engineer. */ "Use web port" = "Usar puerto web"; @@ -5890,6 +6470,9 @@ report reason */ /* No comment provided by engineer. */ "v%@ (%@)" = "v%@ (%@)"; +/* relay test step */ +"Verify" = "Verificar"; + /* No comment provided by engineer. */ "Verify code with desktop" = "Verificar código con ordenador"; @@ -5911,6 +6494,9 @@ report reason */ /* No comment provided by engineer. */ "Verify security code" = "Comprobar código de seguridad"; +/* relay hostname */ +"via %@" = "mediante %@"; + /* No comment provided by engineer. */ "Via browser" = "Mediante navegador"; @@ -5980,9 +6566,18 @@ report reason */ /* No comment provided by engineer. */ "Voice messages prohibited!" = "¡Mensajes de voz no permitidos!"; +/* alert action */ +"Wait" = "Espera"; + +/* relay test step */ +"Wait response" = "Espera respuesta"; + /* No comment provided by engineer. */ "waiting for answer…" = "esperando respuesta…"; +/* No comment provided by engineer. */ +"Waiting for channel owner to add relays." = "Esperando a que el propietario del canal añada servidores."; + /* No comment provided by engineer. */ "waiting for confirmation…" = "esperando confirmación…"; @@ -6013,6 +6608,9 @@ report reason */ /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Atención: ¡puedes perder algunos datos!"; +/* No comment provided by engineer. */ +"We made connecting simpler for new users." = "Hemos simplificado la conexión para los usuarios nuevos."; + /* No comment provided by engineer. */ "WebRTC ICE servers" = "Servidores WebRTC ICE"; @@ -6049,6 +6647,9 @@ report reason */ /* 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." = "Cuando compartes un perfil incógnito con alguien, este perfil también se usará para los grupos a los que te inviten."; +/* No comment provided by engineer. */ +"Why SimpleX is built." = "Por qué fue creado SimpleX."; + /* No comment provided by engineer. */ "WiFi" = "WiFi"; @@ -6148,6 +6749,9 @@ report reason */ /* No comment provided by engineer. */ "you are observer" = "Tu rol es observador"; +/* No comment provided by engineer. */ +"you are subscriber" = "eres suscriptor"; + /* snd group event chat item */ "you blocked %@" = "has bloqueado a %@"; @@ -6191,7 +6795,10 @@ report reason */ "You can set lock screen notification preview via settings." = "Puedes configurar las notificaciones de la pantalla de bloqueo desde Configuración."; /* 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." = "Puedes compartir el enlace o el código QR para que cualquiera pueda unirse al grupo. Si más tarde lo eliminas, no afectará a los miembros del grupo."; +"You can share a link or a QR code - anybody will be able to join the channel." = "Puedes compartir un enlace o código QR. Cualquiera podrá unirse al canal."; + +/* 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." = "Puedes compartir el enlace o código QR. Cualquiera podrá unirse al grupo. Si más tarde lo eliminas, no afectará a los miembros del grupo."; /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Puedes compartir esta dirección con tus contactos para que puedan conectar con **%@**."; @@ -6230,10 +6837,13 @@ report reason */ "you changed role of %@ to %@" = "has cambiado el rol de %1$@ a %2$@"; /* No comment provided by engineer. */ -"You could not be verified; please try again." = "No has podido ser autenticado. Inténtalo de nuevo."; +"You commit to:\n- Only legal content in public groups\n- Respect other users - no spam" = "Te comprometes a:\n- Sólo contenido legal en grupos públicos\n- Respetar a los demás usuarios — no hacer spam"; /* No comment provided by engineer. */ -"You decide who can connect." = "Tu decides quién se conecta."; +"You connected to the channel via this relay link." = "Te conectaste al canal mediante este enlace de servidor."; + +/* No comment provided by engineer. */ +"You could not be verified; please try again." = "No has podido ser autenticado. Inténtalo de nuevo."; /* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Ya has solicitado la conexión\n¿Repetir solicitud?"; @@ -6289,6 +6899,9 @@ report reason */ /* snd group event chat item */ "you unblocked %@" = "has desbloqueado a %@"; +/* No comment provided by engineer. */ +"You were born without an account" = "Naciste sin una cuenta"; + /* No comment provided by engineer. */ "You will be able to send messages **only after your request is accepted**." = "Podrás enviar mensajes **después de que tu solicitud sea aceptada**."; @@ -6311,7 +6924,10 @@ report reason */ "You will still receive calls and notifications from muted profiles when they are active." = "Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos."; /* No comment provided by engineer. */ -"You will stop receiving messages from this chat. Chat history will be preserved." = "Dejarás de recibir mensajes del chat. El historial del chat se conserva."; +"You will stop receiving messages from this channel. Chat history will be preserved." = "Dejarás de recibir mensajes de este canal. El historial del chat se conservará."; + +/* No comment provided by engineer. */ +"You will stop receiving messages from this chat. Chat history will be preserved." = "Dejarás de recibir mensajes del chat. El historial del chat se conservará."; /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Dejarás de recibir mensajes del grupo. El historial del chat se conservará."; @@ -6334,6 +6950,9 @@ report reason */ /* No comment provided by engineer. */ "Your calls" = "Llamadas"; +/* No comment provided by engineer. */ +"Your channel" = "Tu canal"; + /* No comment provided by engineer. */ "Your chat database" = "Base de datos"; @@ -6364,6 +6983,9 @@ report reason */ /* No comment provided by engineer. */ "Your contacts will remain connected." = "Tus contactos permanecerán conectados."; +/* No comment provided by engineer. */ +"Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public." = "Tus conversaciones te pertenecen, tal como ha sido siempre antes de la llegada de internet. Tu red no es un lugar que visitas. Es un lugar que has creado, te pertenece y nadie te la podrá quitar, ya sea pública o privada."; + /* No comment provided by engineer. */ "Your credentials may be sent unencrypted." = "Tus credenciales podrían ser enviadas sin cifrar."; @@ -6379,6 +7001,9 @@ report reason */ /* No comment provided by engineer. */ "Your ICE servers" = "Servidores ICE"; +/* No comment provided by engineer. */ +"Your network" = "Tu red"; + /* No comment provided by engineer. */ "Your preferences" = "Mis preferencias"; @@ -6388,6 +7013,9 @@ report reason */ /* No comment provided by engineer. */ "Your profile" = "Tu perfil"; +/* No comment provided by engineer. */ +"Your profile **%@** will be shared with channel relays and subscribers.\nRelays can access channel messages." = "El perfil **%@** será compartido con los servidores de canal y los suscriptores.\nLos servidores tienen acceso a los mensajes del canal."; + /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "El perfil **%@** será compartido."; @@ -6400,9 +7028,18 @@ report reason */ /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Tu perfil ha sido modificado. Si lo guardas la actualización será enviada a todos tus contactos."; +/* No comment provided by engineer. */ +"Your public address" = "Tu dirección pública"; + /* No comment provided by engineer. */ "Your random profile" = "Tu perfil aleatorio"; +/* No comment provided by engineer. */ +"Your relay address" = "Tu dirección de servidor"; + +/* No comment provided by engineer. */ +"Your relay name" = "Tu nombre del servidor"; + /* No comment provided by engineer. */ "Your server address" = "Dirección del servidor"; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index ea3f9c4386..b75323054a 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -13,15 +13,9 @@ /* No comment provided by engineer. */ "!1 colored!" = "!1 värillinen!"; -/* 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. */ "**e2e encrypted** audio call" = "**e2e-salattu** äänipuhelu"; @@ -257,9 +251,6 @@ swipe action */ /* 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 profile" = "Lisää profiili"; @@ -386,9 +377,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Vastaa puheluun"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia."; - /* No comment provided by engineer. */ "App build: %@" = "Sovellusversio: %@"; @@ -650,7 +638,8 @@ set passcode view */ /* No comment provided by engineer. */ "Confirm password" = "Vahvista salasana"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Yhdistä"; /* No comment provided by engineer. */ @@ -698,7 +687,7 @@ set passcode view */ /* alert title */ "Connection error" = "Yhteysvirhe"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "Yhteysvirhe (AUTH)"; /* chat list item title (it should not be shown */ @@ -746,15 +735,15 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "Jatka"; +/* No comment provided by engineer. */ +"Contribute" = "Osallistu"; + /* No comment provided by engineer. */ "Copy" = "Kopioi"; /* No comment provided by engineer. */ "Core version: v%@" = "Ydinversio: v%@"; -/* No comment provided by engineer. */ -"Create" = "Luo"; - /* server test step */ "Create file" = "Luo tiedosto"; @@ -860,9 +849,6 @@ set passcode view */ /* time unit */ "days" = "päivää"; -/* No comment provided by engineer. */ -"Decentralized" = "Hajautettu"; - /* message decrypt error item */ "Decryption error" = "Salauksen purkuvirhe"; @@ -1097,7 +1083,7 @@ alert button */ /* No comment provided by engineer. */ "Edit group profile" = "Muokkaa ryhmäprofiilia"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "Salli"; /* No comment provided by engineer. */ @@ -1115,9 +1101,6 @@ alert button */ /* 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?"; @@ -1226,7 +1209,7 @@ alert button */ /* No comment provided by engineer. */ "error" = "virhe"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "Virhe"; /* No comment provided by engineer. */ @@ -1429,7 +1412,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Löydä keskustelut nopeammin"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen"; /* No comment provided by engineer. */ @@ -1495,7 +1479,7 @@ snd error text */ /* 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. */ +/* chat link info line */ "Group link" = "Ryhmälinkki"; /* No comment provided by engineer. */ @@ -1597,9 +1581,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "Heti"; -/* No comment provided by engineer. */ -"Immune to spam" = "Immuuni roskapostille ja väärinkäytöksille"; - /* No comment provided by engineer. */ "Import" = "Tuo"; @@ -1664,7 +1645,7 @@ snd error text */ "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)"; +"Install SimpleX Chat for terminal" = "Asenna SimpleX Chat terminaalille"; /* No comment provided by engineer. */ "Instant" = "Heti"; @@ -1681,7 +1662,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "virheelliset keskustelu-tiedot"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "Virheellinen yhteyslinkki"; /* invalid chat item */ @@ -2062,9 +2043,6 @@ snd error text */ /* copied message info in history */ "no text" = "ei tekstiä"; -/* No comment provided by engineer. */ -"No user identifiers." = "Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi."; - /* No comment provided by engineer. */ "Notifications" = "Ilmoitukset"; @@ -2256,9 +2234,6 @@ new chat action */ /* 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"; @@ -2271,9 +2246,6 @@ new chat action */ /* No comment provided by engineer. */ "Profile password" = "Profiilin salasana"; -/* alert message */ -"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."; @@ -2326,13 +2298,10 @@ new chat action */ "Read more" = "Lue lisää"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; +"Read more in our GitHub repository." = "Lue lisää GitHub-arkistosta."; /* 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)."; +"Read more in User Guide." = "Lue lisää Käyttöoppaasta."; /* No comment provided by engineer. */ "Receipts are disabled" = "Kuittaukset pois käytöstä"; @@ -2695,15 +2664,9 @@ chat item action */ /* No comment provided by engineer. */ "Share address" = "Jaa osoite"; -/* alert title */ -"Share address with contacts?" = "Jaa osoite kontakteille?"; - /* No comment provided by engineer. */ "Share link" = "Jaa linkki"; -/* 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"; @@ -2770,6 +2733,9 @@ chat item action */ /* notification title */ "Somebody" = "Joku"; +/* No comment provided by engineer. */ +"Star on GitHub" = "Tähti GitHubissa"; + /* No comment provided by engineer. */ "Start chat" = "Aloita keskustelu"; @@ -2848,7 +2814,8 @@ chat item action */ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Testi epäonnistui vaiheessa %@."; /* No comment provided by engineer. */ @@ -2887,9 +2854,6 @@ chat item action */ /* 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 future of messaging" = "Seuraavan sukupolven yksityisviestit"; - /* No comment provided by engineer. */ "The hash of the previous message is different." = "Edellisen viestin tarkiste on erilainen."; @@ -3055,9 +3019,6 @@ chat item action */ /* No comment provided by engineer. */ "Use .onion hosts" = "Käytä .onion-isäntiä"; -/* No comment provided by engineer. */ -"Use chat" = "Käytä chattia"; - /* new chat action */ "Use current profile" = "Käytä nykyistä profiilia"; @@ -3262,9 +3223,6 @@ chat item action */ /* 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 decide who can connect." = "Kimin bağlanabileceğine siz karar verirsiniz."; - /* 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."; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 2b2a1e98e5..329490f34b 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -25,15 +25,9 @@ /* No comment provided by engineer. */ "(this device v%@)" = "(cet appareil v%@)"; -/* No comment provided by engineer. */ -"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Contribuer](https://github.com/simplex-chat/simplex-chat#contribute)"; - /* No comment provided by engineer. */ "[Send us email](mailto:chat@simplex.chat)" = "[Contact par mail](mailto:chat@simplex.chat)"; -/* No comment provided by engineer. */ -"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Star sur GitHub](https://github.com/simplex-chat/simplex-chat)"; - /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Ajouter un contact** : pour créer un nouveau lien d'invitation."; @@ -392,9 +386,6 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "Connections actives"; -/* 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." = "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."; - /* No comment provided by engineer. */ "Add friends" = "Ajouter des amis"; @@ -638,9 +629,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Répondre à l'appel"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "N'importe qui peut heberger un serveur."; - /* No comment provided by engineer. */ "App build: %@" = "Build de l'app : %@"; @@ -864,7 +852,7 @@ marked deleted chat item preview text */ /* 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. */ +/* chat link info line */ "Business address" = "Adresse professionnelle"; /* No comment provided by engineer. */ @@ -876,9 +864,6 @@ marked deleted chat item preview text */ /* 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)."; -/* No comment provided by engineer. */ -"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "En utilisant SimpleX Chat, vous acceptez de :\n- n'envoyer que du contenu légal dans les groupes publics.\n- respecter les autres utilisateurs - pas de spam."; - /* No comment provided by engineer. */ "call" = "appeler"; @@ -1158,9 +1143,6 @@ set passcode view */ /* No comment provided by engineer. */ "Configure ICE servers" = "Configurer les serveurs ICE"; -/* No comment provided by engineer. */ -"Configure server operators" = "Configurer les opérateurs de serveur"; - /* No comment provided by engineer. */ "Confirm" = "Confirmer"; @@ -1194,7 +1176,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Confirmé"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Se connecter"; /* No comment provided by engineer. */ @@ -1290,7 +1273,7 @@ set passcode view */ /* alert title */ "Connection error" = "Erreur de connexion"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "Erreur de connexion (AUTH)"; /* chat list item title (it should not be shown */ @@ -1377,6 +1360,9 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "Continuer"; +/* No comment provided by engineer. */ +"Contribute" = "Contribuer"; + /* No comment provided by engineer. */ "Conversation deleted!" = "Conversation supprimée !"; @@ -1392,12 +1378,9 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Coin"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Corriger le nom pour %@ ?"; -/* No comment provided by engineer. */ -"Create" = "Créer"; - /* No comment provided by engineer. */ "Create 1-time link" = "Créer un lien unique"; @@ -1548,9 +1531,6 @@ set passcode view */ /* No comment provided by engineer. */ "Debug delivery" = "Livraison de débogage"; -/* No comment provided by engineer. */ -"Decentralized" = "Décentralisé"; - /* message decrypt error item */ "Decryption error" = "Erreur de déchiffrement"; @@ -1936,7 +1916,7 @@ chat item action */ /* No comment provided by engineer. */ "Edit group profile" = "Modifier le profil du groupe"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "Activer"; /* No comment provided by engineer. */ @@ -1963,9 +1943,6 @@ chat item action */ /* No comment provided by engineer. */ "Enable lock" = "Activer le verrouillage"; -/* No comment provided by engineer. */ -"Enable notifications" = "Activer les notifications"; - /* No comment provided by engineer. */ "Enable periodic notifications?" = "Activer les notifications périodiques ?"; @@ -2107,7 +2084,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "erreur"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "Erreur"; /* No comment provided by engineer. */ @@ -2466,7 +2443,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Recherche de message plus rapide"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte"; /* No comment provided by engineer. */ @@ -2487,7 +2465,8 @@ snd error text */ /* No comment provided by engineer. */ "Fix not supported by group member" = "Correction non prise en charge par un membre du groupe"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "Pour le profil de discussion %@ :"; /* No comment provided by engineer. */ @@ -2607,7 +2586,7 @@ snd error text */ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "L'invitation du groupe n'est plus valide, elle a été supprimé par l'expéditeur."; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "Lien du groupe"; /* No comment provided by engineer. */ @@ -2721,9 +2700,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "Immédiatement"; -/* No comment provided by engineer. */ -"Immune to spam" = "Protégé du spam et des abus"; - /* No comment provided by engineer. */ "Import" = "Importer"; @@ -2818,7 +2794,7 @@ snd error text */ "Initial role" = "Rôle initial"; /* No comment provided by engineer. */ -"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Installer [SimpleX Chat pour terminal](https://github.com/simplex-chat/simplex-chat)"; +"Install SimpleX Chat for terminal" = "Installer SimpleX Chat pour terminal"; /* No comment provided by engineer. */ "Instant" = "Instantané"; @@ -2838,7 +2814,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "données de chat invalides"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "Lien de connection invalide"; /* invalid chat item */ @@ -2853,7 +2829,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Confirmation de migration invalide"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Nom invalide !"; /* No comment provided by engineer. */ @@ -3222,9 +3198,6 @@ snd error text */ /* No comment provided by engineer. */ "Migrate device" = "Transférer l'appareil"; -/* No comment provided by engineer. */ -"Migrate from another device" = "Transférer depuis un autre appareil"; - /* No comment provided by engineer. */ "Migrate here" = "Transférer ici"; @@ -3459,9 +3432,6 @@ snd error text */ /* copied message info in history */ "no text" = "aucun texte"; -/* No comment provided by engineer. */ -"No user identifiers." = "Aucun identifiant d'utilisateur."; - /* No comment provided by engineer. */ "Not compatible!" = "Non compatible !"; @@ -3506,7 +3476,7 @@ alert button new chat action */ "Ok" = "Ok"; -/* No comment provided by engineer. */ +/* alert button */ "OK" = "OK"; /* No comment provided by engineer. */ @@ -3575,7 +3545,8 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Seul votre contact peut envoyer des messages vocaux."; -/* alert action */ +/* alert action +alert button */ "Open" = "Ouvrir"; /* No comment provided by engineer. */ @@ -3776,9 +3747,6 @@ new chat action */ /* No comment provided by engineer. */ "Privacy for your customers." = "Respect de la vie privée de vos clients."; -/* No comment provided by engineer. */ -"Privacy redefined" = "La vie privée redéfinie"; - /* No comment provided by engineer. */ "Private filenames" = "Noms de fichiers privés"; @@ -3812,9 +3780,6 @@ new chat action */ /* No comment provided by engineer. */ "Profile theme" = "Thème de profil"; -/* alert message */ -"Profile update will be sent to your contacts." = "La mise à jour du profil sera envoyée à vos contacts."; - /* No comment provided by engineer. */ "Prohibit audio/video calls." = "Interdire les appels audio/vidéo."; @@ -3897,16 +3862,10 @@ new chat action */ "Read more" = "En savoir plus"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Pour en savoir plus, consultez le [Guide de l'utilisateur](https ://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in our GitHub repository." = "Pour en savoir plus, consultez notre dépôt GitHub."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; - -/* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Pour en savoir plus, consultez le [Guide de l'utilisateur](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)." = "Pour en savoir plus, consultez notre [dépôt GitHub](https://github.com/simplex-chat/simplex-chat#readme)."; +"Read more in User Guide." = "Pour en savoir plus, consultez le Guide de l'utilisateur."; /* No comment provided by engineer. */ "Receipts are disabled" = "Les accusés de réception sont désactivés"; @@ -4512,9 +4471,6 @@ chat item action */ /* No comment provided by engineer. */ "Share address publicly" = "Partager publiquement votre adresse"; -/* alert title */ -"Share address with contacts?" = "Partager l'adresse avec vos contacts ?"; - /* No comment provided by engineer. */ "Share from other apps." = "Partager depuis d'autres applications."; @@ -4533,9 +4489,6 @@ chat item action */ /* No comment provided by engineer. */ "Share to SimpleX" = "Partager sur SimpleX"; -/* No comment provided by engineer. */ -"Share with contacts" = "Partager avec vos contacts"; - /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Afficher → sur les messages envoyés via le routage privé."; @@ -4671,6 +4624,9 @@ chat item action */ /* chat item text */ "standard end-to-end encryption" = "chiffrement de bout en bout standard"; +/* No comment provided by engineer. */ +"Star on GitHub" = "Star sur GitHub"; + /* No comment provided by engineer. */ "Start chat" = "Démarrer le chat"; @@ -4764,9 +4720,6 @@ chat item action */ /* No comment provided by engineer. */ "Tap button " = "Appuyez sur le bouton "; -/* No comment provided by engineer. */ -"Tap Create SimpleX address in the menu to create it later." = "Appuyez sur Créer une adresse SimpleX dans le menu pour la créer ultérieurement."; - /* No comment provided by engineer. */ "Tap to activate profile." = "Appuyez pour activer un profil."; @@ -4803,7 +4756,8 @@ chat item action */ /* file error alert title */ "Temporary file error" = "Erreur de fichier temporaire"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Échec du test à l'étape %@."; /* No comment provided by engineer. */ @@ -4854,9 +4808,6 @@ chat item action */ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Le chiffrement fonctionne et le nouvel accord de chiffrement n'est pas nécessaire. Cela peut provoquer des erreurs de connexion !"; -/* No comment provided by engineer. */ -"The future of messaging" = "La nouvelle génération de messagerie privée"; - /* No comment provided by engineer. */ "The hash of the previous message is different." = "Le hash du message précédent est différent."; @@ -5007,9 +4958,6 @@ chat item action */ /* No comment provided by engineer. */ "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. */ -"Toggle chat list:" = "Afficher la liste des conversations :"; - /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Basculer en mode incognito lors de la connexion."; @@ -5175,9 +5123,6 @@ chat item action */ /* No comment provided by engineer. */ "Use %@" = "Utiliser %@"; -/* No comment provided by engineer. */ -"Use chat" = "Utiliser le chat"; - /* new chat action */ "Use current profile" = "Utiliser le profil actuel"; @@ -5565,9 +5510,6 @@ chat item action */ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Vous n'avez pas pu être vérifié·e ; veuillez réessayer."; -/* No comment provided by engineer. */ -"You decide who can connect." = "Vous choisissez qui peut se connecter."; - /* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Vous avez déjà demandé une connexion !\nRépéter la demande de connexion ?"; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 56277f4fd3..623d79433c 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -10,6 +10,9 @@ /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- stabilabb üzenetkézbesítés.\n- picit továbbfejlesztett csoportok.\n- és még sok más!"; +/* No comment provided by engineer. */ +"- opt-in to send link previews.\n- prevent hyperlink phishing.\n- remove link tracking." = "- Hivatkozások előnézetének küldése.\n- Hiperhivatkozásokon keresztüli adathalászat megakadályozása.\n- Hivatkozások nyomonkövetési paramétereinek eltávolítása."; + /* No comment provided by engineer. */ "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- partnerek értesítése a törlésről (nem kötelező)\n- profilnevek szóközökkel\n- és még sok más!"; @@ -19,21 +22,21 @@ /* No comment provided by engineer. */ "!1 colored!" = "!1 színezett!"; +/* chat link info line */ +"(from owner)" = "(a tulajdonostól)"; + /* No comment provided by engineer. */ "(new)" = "(új)"; +/* chat link info line */ +"(signed)" = "(aláírva)"; + /* No comment provided by engineer. */ "(this device v%@)" = "(ez az eszköz: v%@)"; -/* No comment provided by engineer. */ -"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Közreműködés](https://github.com/simplex-chat/simplex-chat#contribute)"; - /* No comment provided by engineer. */ "[Send us email](mailto:chat@simplex.chat)" = "[Küldjön nekünk e-mailt](mailto:chat@simplex.chat)"; -/* No comment provided by engineer. */ -"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Csillagozás a GitHubon](https://github.com/simplex-chat/simplex-chat)"; - /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Partner hozzáadása:** új meghívási hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz."; @@ -50,13 +53,13 @@ "**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Privátabb:** 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken meg lesz osztva a SimpleX Chat kiszolgálóval, de az nem, hogy hány partnere vagy üzenete van."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Legprivátabb:** ne használja a SimpleX Chat értesítési kiszolgálót, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**A legprivátabb**: Az alkalmazás nem használja a SimpleX Chat push-kiszolgálóját. Az alkalmazás a háttérben ellenőrzi az üzeneteket, amikor a rendszer ezt lehetővé teszi, attól függően, hogy Ön milyen gyakran használja az alkalmazást."; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Megjegyzés:** ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja a partnereitől érkező üzenetek visszafejtését."; /* No comment provided by engineer. */ -"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Megjegyzés:** NEM fogja tudni helyreállítani, vagy módosítani a jelmondatot abban az esetben, ha elveszíti."; +"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Megjegyzés:** NEM fogja tudni helyreállítani vagy módosítani a jelmondatot abban az esetben, ha elveszíti."; /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Megjegyzés:** az eszköztoken és az értesítések el lesznek küldve a SimpleX Chat értesítési kiszolgálóra, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik."; @@ -64,6 +67,9 @@ /* No comment provided by engineer. */ "**Scan / Paste link**: to connect via a link you received." = "**Hivatkozás beolvasása / beillesztése**: egy kapott hivatkozáson keresztüli kapcsolódáshoz."; +/* No comment provided by engineer. */ +"**Test relay** to retrieve its name." = "**Átjátszó tesztelése** a nevének lekéréséhez."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Figyelmeztetés:** Az azonnali leküldéses értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges."; @@ -175,6 +181,18 @@ /* time interval */ "%d months" = "%d hónap"; +/* channel relay bar +channel subscriber relay bar */ +"%d relays failed" = "%d átjátszóhoz nem sikerült kapcsolódni"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays not active" = "%d átjátszó inaktív"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays removed" = "%d átjátszó eltávolítva"; + /* time interval */ "%d sec" = "%d mp"; @@ -184,15 +202,50 @@ /* integrity error chat item */ "%d skipped message(s)" = "%d üzenet kihagyva"; +/* channel subscriber count */ +"%d subscriber" = "%d feliratkozó"; + +/* channel subscriber count */ +"%d subscribers" = "%d feliratkozó"; + /* time interval */ "%d weeks" = "%d hét"; +/* channel creation progress +channel relay bar progress */ +"%d/%d relays active" = "%1$d/%2$d átjátszó aktív"; + +/* channel relay bar */ +"%d/%d relays active, %d errors" = "%1$d/%2$d átjátszó aktív, %3$d hiba"; + +/* channel creation progress with errors +channel relay bar */ +"%d/%d relays active, %d failed" = "%1$d/%2$d átjátszó aktív, %3$d sikertelen"; + +/* channel relay bar */ +"%d/%d relays active, %d removed" = "%1$d/%2$d átjátszó aktív, %3$d eltávolítva"; + +/* channel subscriber relay bar progress */ +"%d/%d relays connected" = "%1$d/%2$d átjátszó kapcsolódva"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d errors" = "%1$d/%2$d átjátszó kapcsolódva, %3$d hiba"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d failed" = "%1$d/%2$d átjátszó kapcsolódott, %3$d átjátszóhoz nem sikerült kapcsolódni"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d removed" = "%1$d/%2$d átjátszó kapcsolódott, %3$d eltávolítva"; + /* No comment provided by engineer. */ "%lld" = "%lld"; /* No comment provided by engineer. */ "%lld %@" = "%lld %@"; +/* No comment provided by engineer. */ +"%lld channel events" = "%lld csatornaesemény"; + /* No comment provided by engineer. */ "%lld contact(s) selected" = "%lld partner kiválasztva"; @@ -262,6 +315,9 @@ /* No comment provided by engineer. */ "~strike~" = "\\~áthúzott~"; +/* owner verification */ +"⚠️ Signature verification failed: %@." = "⚠️ Nem sikerült ellenőrizni az aláírást: %@."; + /* time to disappear */ "0 sec" = "0 mp"; @@ -293,7 +349,7 @@ time interval */ "1-time link" = "Egyszer használható meghívó"; /* No comment provided by engineer. */ -"1-time link can be used *with one contact only* - share in person or via any messenger." = "Az egyszer használható meghívó egy hivatkozás és *csak egyetlen partnerrel használható* – személyesen vagy bármilyen üzenetváltó-alkalmazáson keresztül megosztható."; +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Az egyszer használható meghívó egy hivatkozás és *csak egyetlen partnerrel használható* – személyesen vagy bármilyen üzenetváltó alkalmazáson keresztül megosztható."; /* No comment provided by engineer. */ "5 minutes" = "5 perc"; @@ -307,6 +363,9 @@ time interval */ /* No comment provided by engineer. */ "A few more things" = "Néhány további dolog"; +/* No comment provided by engineer. */ +"A link for one person to connect" = "Egy hivatkozás, ami egyetlen partnerrel való kapcsolat létrehozására szolgál"; + /* notification title */ "A new contact" = "Egy új partner"; @@ -371,6 +430,9 @@ swipe action */ /* alert title */ "Accept member" = "Tag befogadása"; +/* No comment provided by engineer. */ +"accepted" = "elfogadva"; + /* rcv group event chat item */ "accepted %@" = "befogadta őt: %@"; @@ -392,6 +454,9 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledgement errors" = "Visszaigazolási hibák"; +/* No comment provided by engineer. */ +"active" = "aktív"; + /* token status text */ "Active" = "Aktív"; @@ -399,7 +464,7 @@ swipe action */ "Active connections" = "Aktív kapcsolatok száma"; /* 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." = "Cím hozzáadása a profilhoz, hogy a partnerei megoszthassák másokkal. A profilfrissítés el lesz küldve partnerei számára."; +"Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts." = "Cím hozzáadása a profilhoz, hogy a SimpleX partnerei megoszthassák másokkal. A profilfrissítés el lesz küldve a SimpleX partnerei számára."; /* No comment provided by engineer. */ "Add friends" = "Barátok hozzáadása"; @@ -440,6 +505,9 @@ swipe action */ /* No comment provided by engineer. */ "Added message servers" = "Hozzáadott üzenetkiszolgálók"; +/* No comment provided by engineer. */ +"Adding relays will be supported later." = "Az átjátszók hozzáadása később lesz támogatott."; + /* No comment provided by engineer. */ "Additional accent" = "További kiemelőszín"; @@ -530,6 +598,12 @@ swipe action */ /* profile dropdown */ "All profiles" = "Összes profil"; +/* No comment provided by engineer. */ +"All relays failed" = "Nem sikerült kapcsolódni egyetlen átjátszóhoz sem"; + +/* No comment provided by engineer. */ +"All relays removed" = "Az összes átjátszó el lett távolítva"; + /* No comment provided by engineer. */ "All reports will be archived for you." = "Az összes jelentés archiválva lesz az Ön számára."; @@ -543,7 +617,7 @@ swipe action */ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Az összes partnerével továbbra is kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára."; /* No comment provided by engineer. */ -"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-továbbítókiszolgálókra."; +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-átjátszókra."; /* No comment provided by engineer. */ "Allow" = "Engedélyezés"; @@ -566,6 +640,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. (24 óra)"; +/* No comment provided by engineer. */ +"Allow members to chat with admins." = "A csevegés az adminisztrátorokkal engedélyezve van a tagok számára."; + /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "A reakciók hozzáadása az üzenetekhez csak abban az esetben van engedélyezve, ha a partnere is engedélyezi."; @@ -575,12 +652,18 @@ swipe action */ /* No comment provided by engineer. */ "Allow sending direct messages to members." = "A közvetlen üzenetek küldése a tagok között engedélyezve van."; +/* No comment provided by engineer. */ +"Allow sending direct messages to subscribers." = "A közvetlen üzenetek küldése a feliratkozók között engedélyezve van."; + /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Az eltűnő üzenetek küldése engedélyezve van."; /* No comment provided by engineer. */ "Allow sharing" = "Megosztás engedélyezése"; +/* No comment provided by engineer. */ +"Allow subscribers to chat with admins." = "A csevegés az adminisztrátorokkal engedélyezve van a feliratkozók számára."; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Az elküldött üzenetek végleges törlése engedélyezve van. (24 óra)"; @@ -636,7 +719,7 @@ swipe action */ "Always use private routing." = "Mindig legyen használva privát útválasztás."; /* No comment provided by engineer. */ -"Always use relay" = "Mindig legyen használva továbbítókiszolgáló"; +"Always use relay" = "Mindig legyen használva átjátszó"; /* No comment provided by engineer. */ "An empty chat profile with the provided name is created, and the app opens as usual." = "Egy üres csevegési profil lesz létrehozva a megadott névvel, és az alkalmazás a szokásos módon megnyílik."; @@ -650,9 +733,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Hívás fogadása"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "Bárki üzemeltethet kiszolgálókat."; - /* No comment provided by engineer. */ "App build: %@" = "Alkalmazás összeállítási száma: %@"; @@ -794,6 +874,15 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "Hibás az üzenet azonosítója"; +/* No comment provided by engineer. */ +"Be free\nin your network" = "Váljon szabaddá\na saját hálózatában"; + +/* No comment provided by engineer. */ +"Be free in your network." = "Legyen szabad a saját hálózatában."; + +/* No comment provided by engineer. */ +"Because we destroyed the power to know who you are. So that your power can never be taken." = "Mert felszámoltuk a lehetőségét is annak, hogy megtudjuk, Ön kicsoda. Így az önrendelkezése soha nem kerülhet idegen kezekbe."; + /* No comment provided by engineer. */ "Better calls" = "Továbbfejlesztett hívásélmény"; @@ -851,6 +940,9 @@ swipe action */ /* No comment provided by engineer. */ "Block member?" = "Letiltja a tagot?"; +/* No comment provided by engineer. */ +"Block subscriber for all?" = "Az összes feliratkozó számára letiltja a feliratkozót?"; + /* marked deleted chat item preview text */ "blocked" = "letiltva"; @@ -895,9 +987,15 @@ marked deleted chat item preview text */ "Both you and your contact can send voice messages." = "Mindkét fél küldhet hangüzeneteket."; /* 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)!" = "Bolgár, finn, thai és ukrán – köszönet a felhasználóknak és a [Weblate-nek](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +"Bottom bar" = "Alsó sáv"; + +/* compose placeholder for channel owner */ +"Broadcast" = "Közvetítés…"; /* 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)!" = "Bolgár, finn, thai és ukrán – köszönet a felhasználóknak és a [Weblate-nek](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + +/* chat link info line */ "Business address" = "Üzleti cím"; /* No comment provided by engineer. */ @@ -912,9 +1010,6 @@ marked deleted chat item preview text */ /* 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)." = "A csevegési profillal (alapértelmezett), vagy a [kapcsolattal] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BÉTA)."; -/* No comment provided by engineer. */ -"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "A SimpleX Chat használatával Ön elfogadja, hogy:\n- csak elfogadott tartalmakat tesz közzé a nyilvános csoportokban.\n- tiszteletben tartja a többi felhasználót, és nem küld kéretlen tartalmat senkinek."; - /* No comment provided by engineer. */ "call" = "hívás"; @@ -939,6 +1034,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Camera not available" = "A kamera nem elérhető"; +/* No comment provided by engineer. */ +"can't broadcast" = "nem lehet közvetíteni"; + /* No comment provided by engineer. */ "Can't call contact" = "Nem lehet felhívni a partnert"; @@ -1038,6 +1136,58 @@ set passcode view */ /* chat item text */ "changing address…" = "cím módosítása…"; +/* shown as sender role for channel messages */ +"channel" = "csatorna"; + +/* No comment provided by engineer. */ +"Channel" = "Csatorna"; + +/* No comment provided by engineer. */ +"Channel display name" = "Csatorna megjelenítendő neve"; + +/* No comment provided by engineer. */ +"Channel full name (optional)" = "Csatorna teljes neve (nem kötelező)"; + +/* alert message +alert subtitle */ +"Channel has no active relays. Please try to join later." = "A csatornának nincsenek aktív átjátszói. Próbáljon meg később csatlakozni."; + +/* No comment provided by engineer. */ +"Channel image" = "Csatornakép"; + +/* chat link info line */ +"Channel link" = "Csatornahivatkozás"; + +/* No comment provided by engineer. */ +"Channel preferences" = "Csatornabeállítások"; + +/* No comment provided by engineer. */ +"Channel profile" = "Csatornaprofil"; + +/* No comment provided by engineer. */ +"Channel profile is stored on subscribers' devices and on the chat relays." = "A csatornaprofil a feliratkozók eszközén és a csevegési átjátszókon van tárolva."; + +/* snd group event chat item */ +"channel profile updated" = "csatornaprofil frissítve"; + +/* alert message */ +"Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers." = "A csatornaprofil módosult. Ha menti, akkor a frissített profil el lesz küldve a csatorna feliratkozóinak."; + +/* alert title */ +"Channel temporarily unavailable" = "A csatorna ideiglenesen nem érhető el"; + +/* No comment provided by engineer. */ +"Channel will be deleted for all subscribers - this cannot be undone!" = "A csatorna az összes feliratkozó számára törölve lesz – ez a művelet nem vonható vissza!"; + +/* No comment provided by engineer. */ +"Channel will be deleted for you - this cannot be undone!" = "A csatorna törölve lesz az Ön számára – ez a művelet nem vonható vissza!"; + +/* alert message */ +"Channel will start working with %d of %d relays. Proceed?" = "A csatorna %2$d átjátszóból %1$d használatával kezd el működni. Folytatja?"; + +/* No comment provided by engineer. */ +"Channels" = "Csatornák"; + /* No comment provided by engineer. */ "Chat" = "Csevegés"; @@ -1089,6 +1239,18 @@ set passcode view */ /* No comment provided by engineer. */ "Chat profile" = "Csevegési profil"; +/* No comment provided by engineer. */ +"Chat relay" = "Csevegési átjátszó"; + +/* No comment provided by engineer. */ +"Chat relays" = "Csevegési átjátszók"; + +/* No comment provided by engineer. */ +"Chat relays forward messages in channels you create." = "A csevegési átjátszók továbbítják az üzeneteket az Ön által létrehozott csatornákban."; + +/* No comment provided by engineer. */ +"Chat relays forward messages to channel subscribers." = "A csevegési átjátszók továbbítják az üzeneteket a csatorna feliratkozóinak."; + /* No comment provided by engineer. */ "Chat theme" = "Csevegés témája"; @@ -1098,7 +1260,8 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "A csevegés törölve lesz az Ön számára – ez a művelet nem vonható vissza!"; -/* chat toolbar */ +/* chat feature +chat toolbar */ "Chat with admins" = "Csevegés az adminisztrátorokkal"; /* No comment provided by engineer. */ @@ -1110,15 +1273,30 @@ set passcode view */ /* No comment provided by engineer. */ "Chats" = "Csevegések"; +/* No comment provided by engineer. */ +"Chats with admins are prohibited." = "A csevegés az adminisztrátorokkal le van tiltva."; + +/* alert message */ +"Chats with admins in public channels have no E2E encryption - use only with trusted chat relays." = "A nyilvános csatornákban az adminisztrátorokkal való csevegések nem rendelkeznek végpontok közötti titkosítással – csak megbízható csevegési átjátszókkal használja őket."; + /* No comment provided by engineer. */ "Chats with members" = "Csevegés a tagokkal"; +/* No comment provided by engineer. */ +"Chats with members are disabled" = "A csevegés a tagokkal le van tiltva"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Üzenetek ellenőrzése 20 percenként."; /* No comment provided by engineer. */ "Check messages when allowed." = "Üzenetek ellenőrzése, amikor engedélyezett."; +/* alert message */ +"Check relay address and try again." = "Ellenőrizze az átjátszó címét, és próbálja újra."; + +/* alert message */ +"Check relay name and try again." = "Ellenőrizze az átjátszó nevét, és próbálja újra."; + /* alert title */ "Check server address and try again." = "Kiszolgáló címének ellenőrzése és újrapróbálkozás."; @@ -1213,7 +1391,7 @@ set passcode view */ "Configure ICE servers" = "ICE-kiszolgálók beállítása"; /* No comment provided by engineer. */ -"Configure server operators" = "Kiszolgálóüzemeltetők beállítása"; +"Configure relays" = "Átjátszók konfigurálása"; /* No comment provided by engineer. */ "Confirm" = "Megerősítés"; @@ -1248,7 +1426,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Megerősítve"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Kapcsolódás"; /* No comment provided by engineer. */ @@ -1278,6 +1457,9 @@ set passcode view */ /* new chat sheet title */ "Connect via link" = "Kapcsolódás egy hivatkozáson keresztül"; +/* No comment provided by engineer. */ +"Connect via link or QR code" = "Hivatkozás vagy QR-kód használata"; + /* new chat sheet title */ "Connect via one-time link" = "Kapcsolódás az egyszer használható meghívón keresztül"; @@ -1347,12 +1529,15 @@ set passcode view */ /* alert title */ "Connection error" = "Kapcsolódási hiba"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "Kapcsolódási hiba (AUTH)"; /* chat list item title (it should not be shown */ "connection established" = "kapcsolat létrehozva"; +/* No comment provided by engineer. */ +"Connection failed" = "Nem sikerült létrehozni a kapcsolatot"; + /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "A kiszolgáló üzemeltetője letiltotta a kapcsolatot:\n%@"; @@ -1389,6 +1574,9 @@ set passcode view */ /* profile update event chat item */ "contact %@ changed to %@" = "%1$@ a következőre módosította a nevét: %2$@"; +/* chat link info line */ +"Contact address" = "Kapcsolattartási cím"; + /* No comment provided by engineer. */ "Contact allows" = "Partner engedélyezi"; @@ -1449,6 +1637,9 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "Folytatás"; +/* No comment provided by engineer. */ +"Contribute" = "Közreműködés"; + /* No comment provided by engineer. */ "Conversation deleted!" = "Beszélgetés törölve!"; @@ -1456,7 +1647,7 @@ set passcode view */ "Copy" = "Másolás"; /* No comment provided by engineer. */ -"Copy error" = "Másolási hiba"; +"Copy error" = "Hiba másolása"; /* No comment provided by engineer. */ "Core version: v%@" = "Fő verzió: v%@"; @@ -1464,12 +1655,9 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Sarok"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Helyesbíti a nevet a következőre: %@?"; -/* No comment provided by engineer. */ -"Create" = "Létrehozás"; - /* No comment provided by engineer. */ "Create 1-time link" = "Egyszer használható meghívó létrehozása"; @@ -1497,6 +1685,12 @@ set passcode view */ /* No comment provided by engineer. */ "Create profile" = "Profil létrehozása"; +/* No comment provided by engineer. */ +"Create public channel" = "Nyilvános csatorna létrehozása"; + +/* No comment provided by engineer. */ +"Create public channel (BETA)" = "Nyilvános csatorna létrehozása (BÉTA)"; + /* server test step */ "Create queue" = "Várólista létrehozása"; @@ -1506,9 +1700,15 @@ set passcode view */ /* No comment provided by engineer. */ "Create your address" = "Saját cím létrehozása"; +/* No comment provided by engineer. */ +"Create your link" = "Saját hivatkozás létrehozása"; + /* No comment provided by engineer. */ "Create your profile" = "Profil létrehozása"; +/* No comment provided by engineer. */ +"Create your public address" = "Saját nyilvános cím létrehozása"; + /* No comment provided by engineer. */ "Created" = "Létrehozva"; @@ -1521,6 +1721,9 @@ set passcode view */ /* No comment provided by engineer. */ "Creating archive link" = "Archívum hivatkozás létrehozása"; +/* No comment provided by engineer. */ +"Creating channel" = "Csatorna létrehozása"; + /* No comment provided by engineer. */ "Creating link…" = "Hivatkozás létrehozása…"; @@ -1623,8 +1826,8 @@ set passcode view */ /* No comment provided by engineer. */ "Debug delivery" = "Kézbesítési hibák felderítése"; -/* No comment provided by engineer. */ -"Decentralized" = "Decentralizált"; +/* relay test step */ +"Decode link" = "Hivatkozás dekódolása"; /* message decrypt error item */ "Decryption error" = "Titkosítás-visszafejtési hiba"; @@ -1667,6 +1870,12 @@ swipe action */ /* No comment provided by engineer. */ "Delete and notify contact" = "Törlés, és a partner értesítése"; +/* No comment provided by engineer. */ +"Delete channel" = "Csatorna törlése"; + +/* No comment provided by engineer. */ +"Delete channel?" = "Törli a csatornát?"; + /* No comment provided by engineer. */ "Delete chat" = "Csevegés törlése"; @@ -1770,6 +1979,9 @@ alert button */ /* server test step */ "Delete queue" = "Várólista törlése"; +/* No comment provided by engineer. */ +"Delete relay" = "Átjátszó törlése"; + /* No comment provided by engineer. */ "Delete report" = "Jelentés törlése"; @@ -1794,6 +2006,9 @@ alert button */ /* copied message info */ "Deleted at: %@" = "Törölve: %@"; +/* rcv group event chat item */ +"deleted channel" = "törölt csatorna"; + /* rcv direct event chat item */ "deleted contact" = "törölt partner"; @@ -1834,13 +2049,13 @@ alert button */ "Desktop devices" = "Számítógépek"; /* No comment provided by engineer. */ -"Destination server address of %@ is incompatible with forwarding server %@ settings." = "A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbítókiszolgáló beállításaival."; +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbító kiszolgáló beállításaival."; /* snd error text */ "Destination server error: %@" = "Célkiszolgáló-hiba: %@"; /* No comment provided by engineer. */ -"Destination server version of %@ is incompatible with forwarding server %@." = "A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbítókiszolgálóval."; +"Destination server version of %@ is incompatible with forwarding server %@." = "A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbító kiszolgálóval."; /* No comment provided by engineer. */ "Detailed statistics" = "Részletes statisztikák"; @@ -1884,6 +2099,12 @@ alert button */ /* No comment provided by engineer. */ "Direct messages between members are prohibited." = "A tagok közötti közvetlen üzenetek le vannak tiltva."; +/* No comment provided by engineer. */ +"Direct messages between subscribers are prohibited." = "A feliratkozók közötti közvetlen üzenetek le vannak tiltva."; + +/* alert button */ +"Disable" = "Letiltás"; + /* No comment provided by engineer. */ "Disable (keep overrides)" = "Letiltás (egyéni beállítások megtartása)"; @@ -1941,6 +2162,9 @@ alert button */ /* No comment provided by engineer. */ "Do not send history to new members." = "Az előzmények ne legyenek elküldve az új tagok számára."; +/* No comment provided by engineer. */ +"Do not send history to new subscribers." = "Az előzmények ne legyenek elküldve az új feliratkozók számára."; + /* No comment provided by engineer. */ "Do NOT send messages directly, even if your or destination server does not support private routing." = "NE küldjön üzeneteket közvetlenül, még akkor sem, ha a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást."; @@ -2020,27 +2244,39 @@ chat item action */ /* No comment provided by engineer. */ "E2E encrypted notifications." = "Végpontok között titkosított értesítések."; +/* No comment provided by engineer. */ +"Easier to invite your friends 👋" = "Könnyebben hívhatja meg a barátait 👋"; + /* chat item action */ "Edit" = "Szerkesztés"; +/* No comment provided by engineer. */ +"Edit channel profile" = "Csatornaprofil szerkesztése"; + /* No comment provided by engineer. */ "Edit group profile" = "Csoportprofil szerkesztése"; /* No comment provided by engineer. */ "Empty message!" = "Üres üzenet!"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "Engedélyezés"; /* No comment provided by engineer. */ "Enable (keep overrides)" = "Engedélyezés (egyéni beállítások megtartása)"; +/* channel creation warning */ +"Enable at least one chat relay in Network & Servers." = "Engedélyezzen legalább egy csevegési átjátszót a „Hálózat és kiszolgálók” menüben."; + /* alert title */ "Enable automatic message deletion?" = "Engedélyezi az automatikus üzenettörlést?"; /* No comment provided by engineer. */ "Enable camera access" = "Kamera-hozzáférés engedélyezése"; +/* alert title */ +"Enable chats with admins?" = "Engedélyezi a csevegést az adminisztrátorokkal?"; + /* No comment provided by engineer. */ "Enable disappearing messages by default." = "Eltűnő üzenetek engedélyezése alapértelmezetten."; @@ -2056,11 +2292,11 @@ chat item action */ /* No comment provided by engineer. */ "Enable instant notifications?" = "Engedélyezi az azonnali értesítéseket?"; -/* No comment provided by engineer. */ -"Enable lock" = "Zárolás engedélyezése"; +/* alert title */ +"Enable link previews?" = "Engedélyezi a hivatkozások előnézetét?"; /* No comment provided by engineer. */ -"Enable notifications" = "Értesítések engedélyezése"; +"Enable lock" = "Zárolás engedélyezése"; /* No comment provided by engineer. */ "Enable periodic notifications?" = "Engedélyezi az időszakos értesítéseket?"; @@ -2167,6 +2403,9 @@ chat item action */ /* call status */ "ended call %@" = "%@ hívása véget ért"; +/* No comment provided by engineer. */ +"Enter channel name…" = "Adja meg a csatorna nevét…"; + /* No comment provided by engineer. */ "Enter correct passphrase." = "Adja meg a helyes jelmondatot."; @@ -2185,6 +2424,12 @@ chat item action */ /* No comment provided by engineer. */ "Enter password above to show!" = "Adja meg a jelszót fentebb a megjelenítéshez!"; +/* No comment provided by engineer. */ +"Enter profile name..." = "Profil nevének megadása…"; + +/* No comment provided by engineer. */ +"Enter relay name…" = "Adja meg az átjátszó nevét…"; + /* No comment provided by engineer. */ "Enter server manually" = "Kiszolgáló megadása kézzel"; @@ -2203,7 +2448,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "hiba"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "Hiba"; /* No comment provided by engineer. */ @@ -2221,6 +2466,9 @@ chat item action */ /* No comment provided by engineer. */ "Error adding member(s)" = "Hiba történt a tag(ok) hozzáadásakor"; +/* alert title */ +"Error adding relay" = "Hiba az átjátszó hozzáadásakor"; + /* alert title */ "Error adding server" = "Hiba történt a kiszolgáló hozzáadásakor"; @@ -2249,7 +2497,7 @@ chat item action */ "Error checking token status" = "Hiba történt a token állapotának ellenőrzésekor"; /* alert message */ -"Error connecting to forwarding server %@. Please try later." = "Hiba történt a(z) %@ továbbítókiszolgálóhoz való kapcsolódáskor. Próbálja meg később."; +"Error connecting to forwarding server %@. Please try later." = "Hiba történt a(z) %@ továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később."; /* subscription status explanation */ "Error connecting to the server used to receive messages from this connection: %@" = "Hiba történt a kapcsolódáskor ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál: %@"; @@ -2257,6 +2505,9 @@ chat item action */ /* No comment provided by engineer. */ "Error creating address" = "Hiba történt a cím létrehozásakor"; +/* alert title */ +"Error creating channel" = "Hiba a csatorna létrehozásakor"; + /* No comment provided by engineer. */ "Error creating group" = "Hiba történt a csoport létrehozásakor"; @@ -2338,9 +2589,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Hiba történt a csevegés megnyitásakor"; -/* No comment provided by engineer. */ -"Error opening group" = "Hiba történt a csoport megnyitásakor"; - /* alert title */ "Error receiving file" = "Hiba történt a fájl fogadásakor"; @@ -2365,6 +2613,9 @@ chat item action */ /* No comment provided by engineer. */ "Error resetting statistics" = "Hiba történt a statisztikák visszaállításakor"; +/* No comment provided by engineer. */ +"Error saving channel profile" = "Hiba a csatornaprofil mentésekor"; + /* alert title */ "Error saving chat list" = "Hiba történt a csevegési lista mentésekor"; @@ -2407,6 +2658,9 @@ chat item action */ /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Hiba történt a kézbesítési jelentések beállításakor!"; +/* alert title */ +"Error sharing channel" = "Hiba a csatorna megosztásakor"; + /* No comment provided by engineer. */ "Error starting chat" = "Hiba történt a csevegés elindításakor"; @@ -2449,12 +2703,16 @@ chat item action */ /* No comment provided by engineer. */ "Error: " = "Hiba: "; +/* receive error chat item */ +"error: %@" = "hiba: %@"; + /* alert message file error text snd error text */ "Error: %@" = "Hiba: %@"; -/* server test error */ +/* relay test error +server test error */ "Error: %@." = "Hiba: %@."; /* No comment provided by engineer. */ @@ -2502,6 +2760,9 @@ snd error text */ /* No comment provided by engineer. */ "Exporting database archive…" = "Adatbázis-archívum exportálása…"; +/* No comment provided by engineer. */ +"failed" = "sikertelen"; + /* No comment provided by engineer. */ "Failed to remove passphrase" = "Nem sikerült eltávolítani a jelmondatot"; @@ -2599,12 +2860,13 @@ snd error text */ "Fingerprint in destination server address does not match certificate: %@." = "A célkiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@."; /* No comment provided by engineer. */ -"Fingerprint in forwarding server address does not match certificate: %@." = "A továbbítókiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@."; +"Fingerprint in forwarding server address does not match certificate: %@." = "A továbbító kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@."; /* No comment provided by engineer. */ "Fingerprint in server address does not match certificate: %@." = "A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@."; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal."; /* No comment provided by engineer. */ @@ -2628,7 +2890,11 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Az összes moderátor számára"; -/* servers error */ +/* No comment provided by engineer. */ +"For anyone to reach you" = "Bárki számára, aki el szeretné érni Önt"; + +/* servers error +servers warning */ "For chat profile %@:" = "A(z) %@ nevű csevegési profilhoz:"; /* No comment provided by engineer. */ @@ -2677,19 +2943,19 @@ snd error text */ "Forwarding %lld messages" = "%lld üzenet továbbítása"; /* alert message */ -"Forwarding server %@ failed to connect to destination server %@. Please try later." = "A(z) %1$@ továbbítókiszolgáló nem tudott kapcsolódni a(z) %2$@ célkiszolgálóhoz. Próbálja meg később."; +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "A(z) %1$@ továbbító kiszolgáló nem tudott kapcsolódni a(z) %2$@ célkiszolgálóhoz. Próbálja meg később."; /* No comment provided by engineer. */ -"Forwarding server address is incompatible with network settings: %@." = "A továbbítókiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@."; +"Forwarding server address is incompatible with network settings: %@." = "A továbbító kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@."; /* No comment provided by engineer. */ -"Forwarding server version is incompatible with network settings: %@." = "A továbbítókiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@."; +"Forwarding server version is incompatible with network settings: %@." = "A továbbító kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@."; /* snd error text */ -"Forwarding server: %@\nDestination server error: %@" = "Továbbítókiszolgáló: %1$@\nCélkiszolgáló-hiba: %2$@"; +"Forwarding server: %@\nDestination server error: %@" = "Továbbító kiszolgáló: %1$@\nCélkiszolgáló-hiba: %2$@"; /* snd error text */ -"Forwarding server: %@\nError: %@" = "Továbbítókiszolgáló: %1$@\nHiba: %2$@"; +"Forwarding server: %@\nError: %@" = "Továbbító kiszolgáló: %1$@\nHiba: %2$@"; /* No comment provided by engineer. */ "Found desktop" = "Megtalált számítógép"; @@ -2712,9 +2978,15 @@ snd error text */ /* No comment provided by engineer. */ "Further reduced battery usage" = "Tovább csökkentett akkumulátor-használat"; +/* relay test step */ +"Get link" = "Hivatkozás megtekintése"; + /* No comment provided by engineer. */ "Get notified when mentioned." = "Kapjon értesítést, ha megemlítik."; +/* No comment provided by engineer. */ +"Get started" = "Vágjunk bele"; + /* No comment provided by engineer. */ "GIFs and stickers" = "GIF-ek és matricák"; @@ -2760,7 +3032,7 @@ snd error text */ /* No comment provided by engineer. */ "group is deleted" = "csoport törölve"; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "Csoporthivatkozás"; /* No comment provided by engineer. */ @@ -2832,6 +3104,9 @@ snd error text */ /* No comment provided by engineer. */ "History is not sent to new members." = "Az előzmények nem lesznek elküldve az új tagok számára."; +/* No comment provided by engineer. */ +"History is not sent to new subscribers." = "Az előzmények nem lesznek elküldve az új feliratkozók számára."; + /* time unit */ "hours" = "óra"; @@ -2871,6 +3146,9 @@ snd error text */ /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő jelkódot:"; +/* down migration warning */ +"If you joined or created channels, they will stop working permanently." = "Ha csatornákat hozott létre vagy csak csatlakozott hozzájuk, akkor azok véglegesen le fognak állni."; + /* No comment provided by engineer. */ "If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Ha most kell használnia a csevegést, koppintson lentebb a **Befejezés később** beállításra (az alkalmazás újraindításakor fel lesz ajánlva az adatbázis átköltöztetése)."; @@ -2889,9 +3167,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "Azonnal"; -/* No comment provided by engineer. */ -"Immune to spam" = "Védett a kéretlen tartalommal szemben"; - /* No comment provided by engineer. */ "Import" = "Importálás"; @@ -2992,7 +3267,7 @@ snd error text */ "Initial role" = "Kezdeti szerepkör"; /* No comment provided by engineer. */ -"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "A [SimpleX Chat terminálhoz] telepítése (https://github.com/simplex-chat/simplex-chat)"; +"Install SimpleX Chat for terminal" = "A SimpleX Chat terminálhoz telepítése"; /* No comment provided by engineer. */ "Instant" = "Azonnali"; @@ -3027,7 +3302,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "érvénytelen csevegésadat"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "Érvénytelen kapcsolattartási hivatkozás"; /* invalid chat item */ @@ -3042,12 +3317,18 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Érvénytelen átköltöztetési visszaigazolás"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Érvénytelen név!"; /* No comment provided by engineer. */ "Invalid QR code" = "Érvénytelen QR-kód"; +/* alert title */ +"Invalid relay address!" = "Érvénytelen az átjátszó címe!"; + +/* alert title */ +"Invalid relay name!" = "Érvénytelen az átjátszó neve!"; + /* No comment provided by engineer. */ "Invalid response" = "Érvénytelen válasz"; @@ -3075,6 +3356,9 @@ snd error text */ /* No comment provided by engineer. */ "Invite members" = "Tagok meghívása"; +/* No comment provided by engineer. */ +"Invite someone privately" = "Partner meghívása privátban"; + /* No comment provided by engineer. */ "Invite to chat" = "Meghívás a csevegésbe"; @@ -3141,6 +3425,9 @@ snd error text */ /* No comment provided by engineer. */ "Join as %@" = "Csatlakozás mint: %@"; +/* No comment provided by engineer. */ +"Join channel" = "Csatlakozás a csatornához"; + /* new chat sheet title */ "Join group" = "Csatlakozás a csoporthoz"; @@ -3189,6 +3476,12 @@ snd error text */ /* swipe action */ "Leave" = "Elhagyás"; +/* No comment provided by engineer. */ +"Leave channel" = "Csatorna elhagyása"; + +/* No comment provided by engineer. */ +"Leave channel?" = "Elhagyja a csatornát?"; + /* No comment provided by engineer. */ "Leave chat" = "Csevegés elhagyása"; @@ -3207,6 +3500,9 @@ snd error text */ /* No comment provided by engineer. */ "Less traffic on mobile networks." = "Kevesebb adatforgalom a mobilhálózatokon."; +/* No comment provided by engineer. */ +"Let someone connect to you" = "Hagyja, hogy valaki elérje Önt"; + /* email subject */ "Let's talk in SimpleX Chat" = "Beszélgessünk a SimpleX Chatben"; @@ -3216,9 +3512,15 @@ snd error text */ /* No comment provided by engineer. */ "Limitations" = "Korlátozások"; +/* No comment provided by engineer. */ +"link" = "hivatkozás"; + /* No comment provided by engineer. */ "Link mobile and desktop apps! 🔗" = "Társítsa össze a hordozható eszköz- és a számítógépes alkalmazásokat! 🔗"; +/* owner verification */ +"Link signature verified." = "Hivatkozás aláírása ellenőrizve."; + /* No comment provided by engineer. */ "Linked desktop options" = "Társított számítógép beállítások"; @@ -3348,6 +3650,9 @@ snd error text */ /* No comment provided by engineer. */ "Members can add message reactions." = "A tagok reakciókat adhatnak hozzá az üzenetekhez."; +/* No comment provided by engineer. */ +"Members can chat with admins." = "A tagok cseveghetnek az adminisztrátorokkal"; + /* No comment provided by engineer. */ "Members can irreversibly delete sent messages. (24 hours)" = "A tagok véglegesen törölhetik az elküldött üzeneteiket. (24 óra)"; @@ -3390,6 +3695,9 @@ snd error text */ /* No comment provided by engineer. */ "Message draft" = "Piszkozatok"; +/* No comment provided by engineer. */ +"Message error" = "Üzenethiba"; + /* item status text */ "Message forwarded" = "Továbbított üzenet"; @@ -3450,6 +3758,12 @@ snd error text */ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "%@ összes üzenete meg fog jelenni!"; +/* No comment provided by engineer. */ +"Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages." = "Ebben a csatornában az üzenetek **nem rendelkeznek végpontok közötti titkosítással**. A csevegési átjátszók láthatják ezeket az üzeneteket."; + +/* E2EE info chat item */ +"Messages in this channel are not end-to-end encrypted. Chat relays can see these messages." = "Ebben a csatornában az üzenetek nem rendelkeznek végpontok közötti titkosítással. A csevegési átjátszók láthatják ezeket az üzeneteket."; + /* alert message */ "Messages in this chat will never be deleted." = "Az ebben a csevegésben lévő üzenetek soha nem lesznek törölve."; @@ -3460,7 +3774,7 @@ snd error text */ "Messages sent" = "Elküldött üzenetek"; /* alert message */ -"Messages were deleted after you selected them." = "Az üzeneteket törölték miután kiváasztotta őket."; +"Messages were deleted after you selected them." = "Az üzeneteket törölték miután kiválasztotta őket."; /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzenetek, a fájlok és a hívások **végpontok közötti titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve."; @@ -3469,10 +3783,10 @@ snd error text */ "Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve."; /* No comment provided by engineer. */ -"Migrate device" = "Eszköz átköltöztetése"; +"Migrate" = "Átköltöztetés"; /* No comment provided by engineer. */ -"Migrate from another device" = "Átköltöztetés egy másik eszközről"; +"Migrate device" = "Eszköz átköltöztetése"; /* No comment provided by engineer. */ "Migrate here" = "Átköltöztetés ide"; @@ -3564,12 +3878,18 @@ snd error text */ /* No comment provided by engineer. */ "Network & servers" = "Hálózat és kiszolgálók"; +/* No comment provided by engineer. */ +"Network commitments" = "Hálózati kötelezettségvállalások"; + /* No comment provided by engineer. */ "Network connection" = "Hálózati kapcsolat"; /* No comment provided by engineer. */ "Network decentralization" = "Hálózati decentralizáció"; +/* conn error description */ +"Network error" = "Hálózati hiba"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "Hálózati problémák – az üzenet többszöri elküldési kísérlet után lejárt."; @@ -3579,6 +3899,9 @@ snd error text */ /* No comment provided by engineer. */ "Network operator" = "Hálózatüzemeltető"; +/* No comment provided by engineer. */ +"Network routers cannot know\nwho talks to whom" = "A hálózati útválasztók nem tudhatják,\nhogy ki kivel beszélget"; + /* No comment provided by engineer. */ "Network settings" = "Hálózati beállítások"; @@ -3588,15 +3911,24 @@ snd error text */ /* delete after time */ "never" = "soha"; +/* No comment provided by engineer. */ +"new" = "új"; + /* token status text */ "New" = "Új"; +/* No comment provided by engineer. */ +"New 1-time link" = "Új egyszer használható meghívó"; + /* No comment provided by engineer. */ "New chat" = "Új csevegés"; /* No comment provided by engineer. */ "New chat experience 🎉" = "Új csevegési élmény 🎉"; +/* No comment provided by engineer. */ +"New chat relay" = "Új csevegési átjátszó"; + /* notification */ "New contact request" = "Új partneri kapcsolatkérés"; @@ -3654,9 +3986,21 @@ snd error text */ /* No comment provided by engineer. */ "No" = "Nem"; +/* No comment provided by engineer. */ +"No account. No phone. No email. No ID.\nThe most secure encryption." = "Nincs fiók. Nincs telefonszám. Nincs e-mail-cím. Nincs személyazonosító.\nA legbiztonságosabb titkosítás."; + +/* No comment provided by engineer. */ +"No active relays" = "Nincsenek aktív átjátszók"; + /* Authentication unavailable */ "No app password" = "Nincs alkalmazás jelszó"; +/* No comment provided by engineer. */ +"No chat relays" = "Nincsenek csevegési átjátszók"; + +/* servers warning */ +"No chat relays enabled." = "Nincsenek engedélyezve csevegési átjátszók."; + /* No comment provided by engineer. */ "No chats" = "Nincsenek csevegések"; @@ -3754,7 +4098,16 @@ snd error text */ "No unread chats" = "Nincsenek olvasatlan csevegések"; /* No comment provided by engineer. */ -"No user identifiers." = "Nincsenek felhasználói azonosítók."; +"Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "Senki sem követte nyomon a beszélgetéseinket. Senki sem készített térképet arról, hogy merre jártunk. A magánéletünk nem csak egy funkció volt, hanem az életmódunk."; + +/* No comment provided by engineer. */ +"Non-profit governance" = "Nonprofit irányítás"; + +/* No comment provided by engineer. */ +"Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "Nem egy jobb zár mások ajtaján. Nem egy kedvesebb házmester, aki tiszteletben tartja az Ön magánéletét, de mégis nyilvántartást vezet minden látogatójáról. Ön itt nem csak egy vendég. Ön itt otthon van. Nincs az a hatalom, amely beléphetne ide - Ön itt szuverén."; + +/* alert title */ +"Not all relays connected" = "Nem minden átjátszó kapcsolódott"; /* No comment provided by engineer. */ "Not compatible!" = "Nem kompatibilis!"; @@ -3812,7 +4165,7 @@ alert button new chat action */ "Ok" = "Rendben"; -/* No comment provided by engineer. */ +/* alert button */ "OK" = "Rendben"; /* No comment provided by engineer. */ @@ -3821,9 +4174,15 @@ new chat action */ /* group pref value */ "on" = "bekapcsolva"; +/* No comment provided by engineer. */ +"On your phone, not on servers." = "Az eszközön, nem pedig kiszolgálókon."; + /* No comment provided by engineer. */ "One-time invitation link" = "Egyszer használható meghívó"; +/* chat link info line */ +"One-time link" = "Egyszer használható meghívó"; + /* No comment provided by engineer. */ "Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Onion kiszolgálók **szükségesek** a kapcsolódáshoz.\nKompatibilis VPN szükséges."; @@ -3833,6 +4192,9 @@ new chat action */ /* No comment provided by engineer. */ "Onion hosts will not be used." = "Az onion kiszolgálók nem lesznek használva."; +/* No comment provided by engineer. */ +"Only channel owners can change channel preferences." = "Csak a csatorna tulajdonosai módosíthatják a csatornabeállításokat."; + /* No comment provided by engineer. */ "Only chat owners can change preferences." = "Csak a csevegés tulajdonosai módosíthatják a csevegési beállításokat."; @@ -3893,12 +4255,16 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Csak a partnere küldhet hangüzeneteket."; -/* alert action */ +/* alert action +alert button */ "Open" = "Megnyitás"; /* No comment provided by engineer. */ "Open changes" = "Módosítások megtekintése"; +/* new chat action */ +"Open channel" = "Csatorna megnyitása"; + /* new chat action */ "Open chat" = "Csevegés megnyitása"; @@ -3911,6 +4277,9 @@ new chat action */ /* No comment provided by engineer. */ "Open conditions" = "Feltételek megnyitása"; +/* alert title */ +"Open external link?" = "Megnyitja a külső hivatkozást?"; + /* alert action */ "Open full link" = "Teljes hivatkozás megnyitása"; @@ -3923,6 +4292,9 @@ new chat action */ /* authentication reason */ "Open migration to another device" = "Átköltöztetés indítása egy másik eszközre"; +/* new chat action */ +"Open new channel" = "Új csatorna megnyitása"; + /* new chat action */ "Open new chat" = "Új csevegés megnyitása"; @@ -3953,6 +4325,9 @@ new chat action */ /* alert title */ "Operator server" = "Kiszolgáló-üzemeltető"; +/* No comment provided by engineer. */ +"Operators commit to:\n- Be independent\n- Minimize metadata usage\n- Run verified open-source code" = "Az üzemeltetők kijelentik, hogy:\n- függetlenek maradnak\n- minimálisra csökkentik a metaadatok használatát\n- ellenőrzött, nyílt forráskódú szoftvereket futtatnak"; + /* No comment provided by engineer. */ "Or import archive file" = "Vagy archívumfájl importálása"; @@ -3965,12 +4340,18 @@ new chat action */ /* No comment provided by engineer. */ "Or securely share this file link" = "Vagy ossza meg biztonságosan ezt a fájlhivatkozást"; +/* No comment provided by engineer. */ +"Or show QR in person or via video call." = "Vagy mutassa meg a QR-kódot személyesen vagy videóhíváson keresztül."; + /* No comment provided by engineer. */ "Or show this code" = "Vagy mutassa meg ezt a kódot"; /* No comment provided by engineer. */ "Or to share privately" = "Vagy a privát megosztáshoz"; +/* No comment provided by engineer. */ +"Or use this QR - print or show online." = "Vagy használja ezt a QR-kódot – nyomtassa ki vagy mutassa meg online."; + /* No comment provided by engineer. */ "Organize chats into lists" = "Csevegések listákba szervezése"; @@ -3989,9 +4370,18 @@ new chat action */ /* member role */ "owner" = "tulajdonos"; +/* No comment provided by engineer. */ +"Owner" = "Tulajdonos"; + /* feature role */ "owners" = "tulajdonosok"; +/* No comment provided by engineer. */ +"Owners" = "Tulajdonosok"; + +/* No comment provided by engineer. */ +"Ownership: you can run your own relays." = "Tulajdonjog: saját átjátszókat üzemeltethet."; + /* No comment provided by engineer. */ "Passcode" = "Jelkód"; @@ -4019,6 +4409,9 @@ new chat action */ /* No comment provided by engineer. */ "Paste image" = "Kép beillesztése"; +/* No comment provided by engineer. */ +"Paste link / Scan" = "Hivatkozás megadása vagy QR-kód beolvasása"; + /* No comment provided by engineer. */ "Paste link to connect!" = "Hivatkozás beillesztése a kapcsolódáshoz!"; @@ -4128,7 +4521,13 @@ new chat action */ "Preserve the last message draft, with attachments." = "Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt."; /* No comment provided by engineer. */ -"Preset server address" = "Az előre beállított kiszolgáló címe"; +"Preset relay address" = "Előre beállított átjátszó címe"; + +/* No comment provided by engineer. */ +"Preset relay name" = "Előre beállított átjátszó neve"; + +/* No comment provided by engineer. */ +"Preset server address" = "Előre beállított kiszolgáló címe"; /* No comment provided by engineer. */ "Preset servers" = "Előre beállított kiszolgálók"; @@ -4149,10 +4548,10 @@ new chat action */ "Privacy policy and conditions of use." = "Adatvédelmi szabályzat és felhasználási feltételek."; /* No comment provided by engineer. */ -"Privacy redefined" = "Újraértelmezett adatvédelem"; +"Privacy: for owners and subscribers." = "Adatvédelem: tulajdonosok és előfizetők számára."; /* No comment provided by engineer. */ -"Private chats, groups and your contacts are not accessible to server operators." = "A privát csevegések, a csoportok és a partnerek nem érhetők el a kiszolgálók üzemeltetői számára."; +"Private and secure messaging." = "Privát és biztonságos üzenetváltás."; /* No comment provided by engineer. */ "Private filenames" = "Privát fájlnevek"; @@ -4178,6 +4577,9 @@ new chat action */ /* alert title */ "Private routing timeout" = "Privát útválasztás időtúllépése"; +/* alert action */ +"Proceed" = "Folytatás"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil és kiszolgálókapcsolatok"; @@ -4194,11 +4596,14 @@ new chat action */ "Profile theme" = "Profiltéma"; /* alert message */ -"Profile update will be sent to your contacts." = "A profilfrissítés el lesz küldve a partnerei számára."; +"Profile update will be sent to your SimpleX contacts." = "A profilfrissítés el lesz küldve a SimpleX partnerei számára."; /* No comment provided by engineer. */ "Prohibit audio/video calls." = "A hívások kezdeményezése le van tiltva."; +/* No comment provided by engineer. */ +"Prohibit chats with admins." = "A csevegés az adminisztrátorokkal le van tiltva."; + /* No comment provided by engineer. */ "Prohibit irreversible message deletion." = "Az elküldött üzenetek végleges törlése le van tiltva."; @@ -4214,6 +4619,9 @@ new chat action */ /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "A közvetlen üzenetek küldése a tagok között le van tiltva."; +/* No comment provided by engineer. */ +"Prohibit sending direct messages to subscribers." = "A közvetlen üzenetek küldése a feliratkozók között le van tiltva."; + /* No comment provided by engineer. */ "Prohibit sending disappearing messages." = "Az eltűnő üzenetek küldése le van tiltva."; @@ -4236,7 +4644,7 @@ new chat action */ "Protect your chat profiles with a password!" = "Védje meg a csevegési profiljait egy jelszóval!"; /* No comment provided by engineer. */ -"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Védje az IP-címét a partnerei által kiválasztott üzenetváltási továbbítókiszolgálókkal szemben.\nEngedélyezze a *Hálózat és kiszolgálók* menüben."; +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Védje az IP-címét a partnerei által kiválasztott üzenetváltási átjátszókkal szemben.\nEngedélyezze a *Hálózat és kiszolgálók* menüben."; /* No comment provided by engineer. */ "Protocol background timeout" = "Protokoll időtúllépése a háttérben"; @@ -4256,6 +4664,9 @@ new chat action */ /* No comment provided by engineer. */ "Proxy requires password" = "A proxy jelszót igényel"; +/* No comment provided by engineer. */ +"Public channels - speak freely 🚀" = "Nyilvános csatornák – mondja el szabadon a véleményét 🚀"; + /* No comment provided by engineer. */ "Push notifications" = "Leküldéses értesítések"; @@ -4284,16 +4695,10 @@ new chat action */ "Read more" = "Tudjon meg többet"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in our GitHub repository." = "További információ a GitHub-tárolónkban."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; - -/* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "További információ a [Használati útmutatóban](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)." = "További információ a [GitHub-tárolónkban](https://github.com/simplex-chat/simplex-chat#readme)."; +"Read more in User Guide." = "További információ a Használati útmutatóban."; /* No comment provided by engineer. */ "Receipts are disabled" = "A kézbesítési jelentések le vannak tiltva"; @@ -4402,11 +4807,35 @@ swipe action */ /* call status */ "rejected call" = "elutasított hívás"; -/* No comment provided by engineer. */ -"Relay server is only used if necessary. Another party can observe your IP address." = "A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét."; +/* member role */ +"relay" = "átjátszó"; /* No comment provided by engineer. */ -"Relay server protects your IP address, but it can observe the duration of the call." = "A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát."; +"Relay" = "Átjátszó"; + +/* alert title */ +"Relay address" = "Átjátszó címe"; + +/* alert title */ +"Relay connection failed" = "Nem sikerült kapcsolódni az átjátszóhoz"; + +/* No comment provided by engineer. */ +"Relay link" = "Átjátszóhivatkozás"; + +/* alert message */ +"Relay results:" = "Átjátszóeredmények:"; + +/* No comment provided by engineer. */ +"Relay server is only used if necessary. Another party can observe your IP address." = "Az átjátszó csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét."; + +/* No comment provided by engineer. */ +"Relay server protects your IP address, but it can observe the duration of the call." = "Az átjátszó megvédi az IP-címét, de megfigyelheti a hívás időtartamát."; + +/* No comment provided by engineer. */ +"Relay test failed!" = "Nem sikerült tesztelni az átjátszót!"; + +/* No comment provided by engineer. */ +"Reliability: many relays per channel." = "Megbízhatóság: több átjátszó is használható csatornánként."; /* alert action */ "Remove" = "Eltávolítás"; @@ -4432,12 +4861,24 @@ swipe action */ /* No comment provided by engineer. */ "Remove passphrase from keychain?" = "Eltávolítja a jelmondatot a kulcstartóból?"; +/* No comment provided by engineer. */ +"Remove subscriber" = "Feliratkozó eltávolítása"; + +/* alert title */ +"Remove subscriber?" = "Eltávolítja a feliratkozót?"; + /* No comment provided by engineer. */ "removed" = "eltávolítva"; +/* receive error chat item */ +"removed (%d attempts)" = "eltávolítva (%d kísérlet)"; + /* rcv group event chat item */ "removed %@" = "eltávolította őt: %@"; +/* No comment provided by engineer. */ +"removed by operator" = "az üzemeltető eltávolította"; + /* profile update event chat item */ "removed contact address" = "eltávolította a kapcsolattartási címet"; @@ -4606,6 +5047,9 @@ swipe action */ /* No comment provided by engineer. */ "Run chat" = "Csevegési szolgáltatás indítása"; +/* No comment provided by engineer. */ +"Safe web links" = "Biztonságos webhivatkozások"; + /* No comment provided by engineer. */ "Safely receive files" = "Fájlok biztonságos fogadása"; @@ -4622,6 +5066,9 @@ chat item action */ /* alert button */ "Save (and notify members)" = "Mentés (és a tagok értesítése)"; +/* alert button */ +"Save (and notify subscribers)" = "Mentés (és a feliratkozók értesítése)"; + /* alert title */ "Save admission settings?" = "Menti a befogadási beállításokat?"; @@ -4631,12 +5078,21 @@ chat item action */ /* No comment provided by engineer. */ "Save and notify group members" = "Mentés és a csoporttagok értesítése"; +/* No comment provided by engineer. */ +"Save and notify subscribers" = "Mentés és a feliratkozók értesítése"; + /* No comment provided by engineer. */ "Save and reconnect" = "Mentés és újrakapcsolódás"; /* No comment provided by engineer. */ "Save and update group profile" = "Mentés és a csoportprofil frissítése"; +/* No comment provided by engineer. */ +"Save channel profile" = "Csatornaprofil mentése"; + +/* alert title */ +"Save channel profile?" = "Menti a csatornaprofilt?"; + /* No comment provided by engineer. */ "Save group profile" = "Csoportprofil mentése"; @@ -4731,7 +5187,7 @@ chat item action */ "Search links" = "Hivatkozások keresése"; /* No comment provided by engineer. */ -"Search or paste SimpleX link" = "Keresés vagy SimpleX-hivatkozás beillesztése"; +"Search or paste SimpleX link" = "Keressen vagy adjon meg egy SimpleX-hivatkozást"; /* No comment provided by engineer. */ "Search videos" = "Videók keresése"; @@ -4766,6 +5222,9 @@ chat item action */ /* chat item text */ "security code changed" = "biztonsági kódja módosult"; +/* No comment provided by engineer. */ +"Security: owners hold channel keys." = "Biztonság: a csatornák kulcsait a tulajdonosok őrzik."; + /* chat item action */ "Select" = "Kiválasztás"; @@ -4844,12 +5303,18 @@ chat item action */ /* No comment provided by engineer. */ "Send request without message" = "Kérés küldése üzenet nélkül"; +/* No comment provided by engineer. */ +"Send the link via any messenger - it's secure. Ask to paste into SimpleX." = "Küldje el a hivatkozást bármilyen üzenetváltó alkalmazáson keresztül – ez egy biztonságos módszer – és kérje meg a partnerét, hogy illessze be a SimpleX alkalmazásba."; + /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Küldje el őket a galériából vagy az egyéni billentyűzetekről."; /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Legfeljebb az utolsó 100 üzenet elküldése az új tagok számára."; +/* No comment provided by engineer. */ +"Send up to 100 last messages to new subscribers." = "Legfeljebb az utolsó 100 üzenet elküldése az új feliratkozók számára."; + /* No comment provided by engineer. */ "Send your private feedback to groups." = "Küldjön privát visszajelzést a csoportoknak."; @@ -4859,6 +5324,9 @@ chat item action */ /* No comment provided by engineer. */ "Sender may have deleted the connection request." = "A kérés küldője törölhette a kapcsolódási kérést."; +/* alert message */ +"Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later." = "A hivatkozáselőnézet küldése felfedheti az Ön IP-címét a weboldal számára. Ezt később módosíthatja az adatvédelmi beállításokban."; + /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "A kézbesítési jelentések küldése engedélyezve lesz az összes látható csevegési profilban lévő összes partnere számára."; @@ -4937,6 +5405,9 @@ chat item action */ /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "a kiszolgáló várólista információi: %1$@\n\nutoljára fogadott üzenet: %2$@"; +/* relay test error */ +"Server requires authorization to connect to relay, check password." = "A kiszolgáló hitelesítést igényel az átjátszóhoz való kapcsolódáshoz, ellenőrizze a jelszavát."; + /* server test error */ "Server requires authorization to create queues, check password." = "A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze a jelszavát."; @@ -5021,6 +5492,12 @@ chat item action */ /* alert message */ "Settings were changed." = "A beállítások módosultak."; +/* No comment provided by engineer. */ +"Setup notifications" = "Értesítések beállítása"; + +/* No comment provided by engineer. */ +"Setup routers" = "Útválasztók beállítása"; + /* No comment provided by engineer. */ "Shape profile images" = "Profilkép alakzata"; @@ -5041,7 +5518,10 @@ chat item action */ "Share address publicly" = "Cím nyilvános megosztása"; /* alert title */ -"Share address with contacts?" = "Megosztja a címet a partnereivel?"; +"Share address with SimpleX contacts?" = "Megosztja a címet a SimpleX partnereivel?"; + +/* No comment provided by engineer. */ +"Share channel" = "Csatorna megosztása"; /* No comment provided by engineer. */ "Share from other apps." = "Megosztás más alkalmazásokból."; @@ -5058,6 +5538,9 @@ chat item action */ /* No comment provided by engineer. */ "Share profile" = "Profil megosztása"; +/* No comment provided by engineer. */ +"Share relay address" = "Átjátszó címének megosztása"; + /* No comment provided by engineer. */ "Share SimpleX address on social media." = "SimpleX-cím megosztása a közösségi médiában."; @@ -5068,7 +5551,10 @@ chat item action */ "Share to SimpleX" = "Megosztás a SimpleXben"; /* No comment provided by engineer. */ -"Share with contacts" = "Megosztás a partnerekkel"; +"Share via chat" = "Megosztás egy csevegésen keresztül"; + +/* No comment provided by engineer. */ +"Share with SimpleX contacts" = "Megosztás a SimpleX partnerekkel"; /* No comment provided by engineer. */ "Share your address" = "Saját cím megosztása"; @@ -5119,7 +5605,7 @@ chat item action */ "SimpleX Address" = "SimpleX-cím"; /* No comment provided by engineer. */ -"SimpleX address and 1-time links are safe to share via any messenger." = "A SimpleX-cím és az egyszer használható meghívó biztonságosan megosztható bármilyen üzenetváltó-alkalmazáson keresztül."; +"SimpleX address and 1-time links are safe to share via any messenger." = "A SimpleX-cím és az egyszer használható meghívó biztonságosan megosztható bármilyen üzenetváltó alkalmazáson keresztül."; /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "SimpleX-cím vagy egyszer használható meghívó?"; @@ -5173,7 +5659,7 @@ chat item action */ "SimpleX protocols reviewed by Trail of Bits." = "A SimpleX protokollokat a Trail of Bits auditálta."; /* simplex link type */ -"SimpleX relay link" = "SimpleX továbbítókiszolgáló-hivatkozás"; +"SimpleX relay address" = "SimpleX-átjátszó címe"; /* No comment provided by engineer. */ "Simplified incognito mode" = "Egyszerűsített inkognitómód"; @@ -5227,6 +5713,9 @@ report reason */ /* chat item text */ "standard end-to-end encryption" = "szabványos végpontok közötti titkosítás"; +/* No comment provided by engineer. */ +"Star on GitHub" = "Csillagozás a GitHubon"; + /* No comment provided by engineer. */ "Start chat" = "Csevegés elindítása"; @@ -5293,6 +5782,48 @@ report reason */ /* No comment provided by engineer. */ "Subscribed" = "Feliratkozva"; +/* No comment provided by engineer. */ +"Subscriber" = "Feliratkozó"; + +/* chat feature */ +"Subscriber reports" = "Feliratkozók jelentései"; + +/* alert message */ +"Subscriber will be removed from channel - this cannot be undone!" = "A feliratkozó el lesz távolítva a csatornából – ez a művelet nem vonható vissza!"; + +/* No comment provided by engineer. */ +"Subscribers" = "Feliratkozók"; + +/* No comment provided by engineer. */ +"Subscribers can add message reactions." = "A feliratkozók reakciókat adhatnak hozzá az üzenetekhez."; + +/* No comment provided by engineer. */ +"Subscribers can chat with admins." = "A feliratkozók cseveghetnek az adminisztrátorokkal."; + +/* No comment provided by engineer. */ +"Subscribers can irreversibly delete sent messages. (24 hours)" = "A feliratkozók véglegesen törölhetik az elküldött üzeneteiket. (24 óra)"; + +/* No comment provided by engineer. */ +"Subscribers can report messsages to moderators." = "A feliratkozók jelenthetik az üzeneteket a moderátorok felé."; + +/* No comment provided by engineer. */ +"Subscribers can send direct messages." = "A feliratkozók küldhetnek egymásnak közvetlen üzeneteket."; + +/* No comment provided by engineer. */ +"Subscribers can send disappearing messages." = "A feliratkozók küldhetnek eltűnő üzeneteket."; + +/* No comment provided by engineer. */ +"Subscribers can send files and media." = "A feliratkozók küldhetnek fájlokat és médiatartalmakat."; + +/* No comment provided by engineer. */ +"Subscribers can send SimpleX links." = "A feliratkozók küldhetnek SimpleX-hivatkozásokat."; + +/* No comment provided by engineer. */ +"Subscribers can send voice messages." = "A feliratkozók küldhetnek hangüzeneteket."; + +/* No comment provided by engineer. */ +"Subscribers use relay link to connect to the channel.\nRelay address was used to set up this relay for the channel." = "A feliratkozók az átjátszó hivatkozását használják a csatornához való kapcsolódáshoz.\nAz átjátszó címe ennek az átjátszónak a beállítására szolgált a csatornához."; + /* No comment provided by engineer. */ "Subscription errors" = "Feliratkozási hibák"; @@ -5320,6 +5851,9 @@ report reason */ /* No comment provided by engineer. */ "Take picture" = "Kép készítése"; +/* No comment provided by engineer. */ +"Talk to someone" = "Beszélgessen valakivel"; + /* No comment provided by engineer. */ "Tap button " = "Koppintson a "; @@ -5333,7 +5867,7 @@ report reason */ "Tap Connect to use bot" = "Koppintson a „Kapcsolódás” gombra a bot használatához"; /* No comment provided by engineer. */ -"Tap Create SimpleX address in the menu to create it later." = "Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz."; +"Tap Join channel" = "Koppintson a „Csatlakozás a csatornához” gombra"; /* No comment provided by engineer. */ "Tap Join group" = "Koppintson a „Csatlakozás a csoporthoz” gombra"; @@ -5350,6 +5884,9 @@ report reason */ /* No comment provided by engineer. */ "Tap to join incognito" = "Koppintson ide az inkognitóban való kapcsolódáshoz"; +/* No comment provided by engineer. */ +"Tap to open" = "Koppintson ide a megnyitáshoz"; + /* No comment provided by engineer. */ "Tap to paste link" = "Koppintson ide a hivatkozás beillesztéséhez"; @@ -5380,12 +5917,16 @@ report reason */ /* file error alert title */ "Temporary file error" = "Ideiglenes fájlhiba"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "A teszt a(z) %@ lépésnél sikertelen volt."; /* No comment provided by engineer. */ "Test notifications" = "Értesítések tesztelése"; +/* No comment provided by engineer. */ +"Test relay" = "Átjátszó tesztelése"; + /* No comment provided by engineer. */ "Test server" = "Kiszolgáló tesztelése"; @@ -5413,6 +5954,9 @@ report reason */ /* No comment provided by engineer. */ "The app protects your privacy by using different operators in each conversation." = "Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetéshez más-más üzemeltetőt használ."; +/* No comment provided by engineer. */ +"The app removed this message after %lld attempts to receive it." = "Az alkalmazás %lld sikertelen letöltési kísérlet után eltávolította ezt az üzenetet."; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról (kivéve .onion) történő letöltések megerősítését."; @@ -5422,6 +5966,9 @@ report reason */ /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "A beolvasott QR-kód nem egy SimpleX-hivatkozás."; +/* conn error description */ +"The connection reached the limit of undelivered messages" = "A kapcsolat elérte a kézbesítetlen üzenetek korlátját"; + /* No comment provided by engineer. */ "The connection reached the limit of undelivered messages, your contact may be offline." = "A kapcsolat elérte a kézbesítetlen üzenetek számának határát, a partnere lehet, hogy offline állapotban van."; @@ -5438,7 +5985,7 @@ report reason */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet!"; /* No comment provided by engineer. */ -"The future of messaging" = "Az üzenetváltás jövője"; +"The first network where you own\nyour contacts and groups." = "Az első hálózat, ahol Ön birtokolja\na saját kapcsolatait és csoportjait."; /* No comment provided by engineer. */ "The hash of the previous message is different." = "Az előző üzenet kivonata különbözik."; @@ -5464,6 +6011,9 @@ report reason */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "A régi adatbázis nem lett eltávolítva az átköltöztetéskor, ezért törölhető."; +/* No comment provided by engineer. */ +"The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it." = "A legrégebbi emberi szabadság - beszélgetni az emberekkel, anélkül, hogy mások megfigyelnének - olyan infrastruktúrán alapul, amely nem tudja elárulni."; + /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető számára is: **%@**."; @@ -5491,6 +6041,12 @@ report reason */ /* No comment provided by engineer. */ "Themes" = "Témák"; +/* No comment provided by engineer. */ +"Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible." = "Aztán felléptünk az internetre, és minden platform kért belőlünk egy darabot - nevet, telefonszámot, baráti kapcsolatokat. Elfogadtuk, hogy a kommunikáció ára az, hogy mások megtudják, hogy kivel beszélünk. Minden generáció, az emberek és a technológia is eddig így működött - telefon, e-mail, üzenetküldő programok, közösségi média. Úgy tűnt, ez az egyetlen lehetséges mód."; + +/* No comment provided by engineer. */ +"There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected." = "De van egy másik lehetőség is. Egy hálózat, amelyben nincsenek telefonszámok. Nincsenek felhasználónevek. Nincsenek fiókok. Nincsenek semmiféle felhasználói azonosítók. Egy hálózat, amely összeköti az embereket és titkosított üzeneteket továbbít, anélkül, hogy tudná, ki csatlakozik hozzá."; + /* No comment provided by engineer. */ "These conditions will also apply for: **%@**." = "Ezek a feltételek lesznek elfogadva a következő számára is: **%@**."; @@ -5533,6 +6089,12 @@ report reason */ /* No comment provided by engineer. */ "This group no longer exists." = "Ez a csoport már nem létezik."; +/* alert message */ +"This is a chat relay address, it cannot be used to connect." = "Ez egy csevegési átjátszó címe, nem használható kapcsolódásra."; + +/* new chat action */ +"This is your link for channel %@!" = "Ez a saját hivatkozása a(z) %@ nevű csatornához!"; + /* No comment provided by engineer. */ "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Ez a hivatkozás újabb alkalmazásverziót igényel. Frissítse az alkalmazást vagy kérjen egy kompatibilis hivatkozást a partnerétől."; @@ -5566,6 +6128,9 @@ report reason */ /* No comment provided by engineer. */ "To make a new connection" = "Új kapcsolat létrehozásához"; +/* No comment provided by engineer. */ +"To make SimpleX Network last." = "A SimpleX hálózat hosszú távú működésének biztosítása érdekében."; + /* No comment provided by engineer. */ "To protect against your link being replaced, you can compare contact security codes." = "A hivatkozás cseréje elleni védelem érdekében összehasonlíthatja a biztonsági kódokat a partnerével."; @@ -5614,9 +6179,6 @@ report reason */ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "A végpontok közötti titkosítás ellenőrzéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) a partnere eszközén lévő kóddal."; -/* No comment provided by engineer. */ -"Toggle chat list:" = "Csevegési lista ki/be:"; - /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Inkognitó profil használata kapcsolódáskor ki/be."; @@ -5626,6 +6188,9 @@ report reason */ /* No comment provided by engineer. */ "Toolbar opacity" = "Eszköztár átlátszatlansága"; +/* No comment provided by engineer. */ +"Top bar" = "Felső sáv"; + /* No comment provided by engineer. */ "Total" = "Összes kapcsolat"; @@ -5665,6 +6230,9 @@ report reason */ /* No comment provided by engineer. */ "Unblock member?" = "Feloldja a tag letiltását?"; +/* No comment provided by engineer. */ +"Unblock subscriber for all?" = "Az összes feliratkozó számára feloldja a feliratkozó letiltását?"; + /* rcv group event chat item */ "unblocked %@" = "feloldotta %@ letiltását"; @@ -5737,12 +6305,15 @@ report reason */ /* swipe action */ "Unread" = "Olvasatlan"; -/* No comment provided by engineer. */ +/* conn error description */ "Unsupported connection link" = "Nem támogatott kapcsolattartási hivatkozás"; /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Legfeljebb az utolsó 100 üzenet lesz elküldve az új tagok számára."; +/* No comment provided by engineer. */ +"Up to 100 last messages are sent to new subscribers." = "Legfeljebb az utolsó 100 üzenet lesz elküldve az új feliratkozók számára."; + /* No comment provided by engineer. */ "Update" = "Frissítés"; @@ -5755,6 +6326,9 @@ report reason */ /* No comment provided by engineer. */ "Update settings?" = "Frissíti a beállításokat?"; +/* rcv group event chat item */ +"updated channel profile" = "frissített csatornaprofil"; + /* No comment provided by engineer. */ "Updated conditions" = "Frissített feltételek"; @@ -5812,9 +6386,6 @@ report reason */ /* No comment provided by engineer. */ "Use %@" = "%@ használata"; -/* No comment provided by engineer. */ -"Use chat" = "SimpleX Chat használata"; - /* new chat action */ "Use current profile" = "Jelenlegi profil használata"; @@ -5824,6 +6395,9 @@ report reason */ /* No comment provided by engineer. */ "Use for messages" = "Használat az üzenetekhez"; +/* No comment provided by engineer. */ +"Use for new channels" = "Használat új csatornákhoz"; + /* No comment provided by engineer. */ "Use for new connections" = "Használat új kapcsolatokhoz"; @@ -5848,6 +6422,9 @@ report reason */ /* No comment provided by engineer. */ "Use private routing with unknown servers." = "Privát útválasztás használata az ismeretlen kiszolgálókhoz."; +/* No comment provided by engineer. */ +"Use relay" = "Átjátszó használata"; + /* No comment provided by engineer. */ "Use server" = "Kiszolgáló használata"; @@ -5872,6 +6449,9 @@ report reason */ /* No comment provided by engineer. */ "Use the app with one hand." = "Alkalmazás egy kézzel való használata."; +/* No comment provided by engineer. */ +"Use this address in your social media profile, website, or email signature." = "Használja ezt a címet a közösségi oldalakon használt profiljaiban, weboldalakon vagy az e-mail aláírásában."; + /* No comment provided by engineer. */ "Use web port" = "Webport használata"; @@ -5890,6 +6470,9 @@ report reason */ /* No comment provided by engineer. */ "v%@ (%@)" = "v%@ (%@)"; +/* relay test step */ +"Verify" = "Ellenőrzés"; + /* No comment provided by engineer. */ "Verify code with desktop" = "Kód ellenőrzése a számítógépen"; @@ -5911,6 +6494,9 @@ report reason */ /* No comment provided by engineer. */ "Verify security code" = "Biztonsági kód ellenőrzése"; +/* relay hostname */ +"via %@" = "a következőn keresztül: %@"; + /* No comment provided by engineer. */ "Via browser" = "Böngészőn keresztül"; @@ -5924,7 +6510,7 @@ report reason */ "via one-time link" = "egy egyszer használható meghívón keresztül"; /* No comment provided by engineer. */ -"via relay" = "továbbítókiszolgálón keresztül"; +"via relay" = "átjátszón keresztül"; /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Biztonságos kvantumbiztos protokollon keresztül."; @@ -5980,9 +6566,18 @@ report reason */ /* No comment provided by engineer. */ "Voice messages prohibited!" = "A hangüzenetek le vannak tiltva!"; +/* alert action */ +"Wait" = "Várakozás"; + +/* relay test step */ +"Wait response" = "Várakozás a válaszra"; + /* No comment provided by engineer. */ "waiting for answer…" = "várakozás a válaszra…"; +/* No comment provided by engineer. */ +"Waiting for channel owner to add relays." = "Várakozás a csatorna tulajdonosára az átjátszók hozzáadásához."; + /* No comment provided by engineer. */ "waiting for confirmation…" = "várakozás a visszaigazolásra…"; @@ -6013,6 +6608,9 @@ report reason */ /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Figyelmeztetés: néhány adat elveszhet!"; +/* No comment provided by engineer. */ +"We made connecting simpler for new users." = "Az új felhasználók számára egyszerűbbé tettük a kapcsolatok létrehozását."; + /* No comment provided by engineer. */ "WebRTC ICE servers" = "WebRTC ICE-kiszolgálók"; @@ -6049,6 +6647,9 @@ report reason */ /* 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." = "Ha egy inkognitóprofilt oszt meg valamelyik partnerével, a rendszer ezt az inkognitóprofilt fogja használni azokban a csoportokban, ahová az adott partnere meghívja Önt."; +/* No comment provided by engineer. */ +"Why SimpleX is built." = "Miért jött létre a SimpleX?"; + /* No comment provided by engineer. */ "WiFi" = "Wi-Fi"; @@ -6071,7 +6672,7 @@ report reason */ "Without Tor or VPN, your IP address will be visible to file servers." = "Tor vagy VPN nélkül az IP-címe láthatóvá válik a fájlkiszolgálók számára."; /* alert message */ -"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-továbbítókiszolgálók számára: %@."; +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-átjátszók számára: %@."; /* No comment provided by engineer. */ "Wrong database passphrase" = "Érvénytelen adatbázis-jelmondat"; @@ -6148,6 +6749,9 @@ report reason */ /* No comment provided by engineer. */ "you are observer" = "Ön megfigyelő"; +/* No comment provided by engineer. */ +"you are subscriber" = "Ön feliratkozó"; + /* snd group event chat item */ "you blocked %@" = "Ön letiltotta őt: %@"; @@ -6190,6 +6794,9 @@ report reason */ /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "A lezárási képernyő értesítési előnézetét az „Értesítések” menüben állíthatja be."; +/* No comment provided by engineer. */ +"You can share a link or a QR code - anybody will be able to join the channel." = "Megoszthat egy hivatkozást vagy egy QR-kódot – bárki képes lesz csatlakozni a csatornához."; + /* 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." = "Megoszthat egy hivatkozást vagy QR-kódot – így bárki csatlakozhat a csoporthoz. Ha a csoporthivatkozást később törli, akkor nem fogja elveszíteni a csoport meglévő tagjait."; @@ -6230,10 +6837,13 @@ report reason */ "you changed role of %@ to %@" = "Ön a következőre módosította %1$@ szerepkörét: „%2$@”"; /* No comment provided by engineer. */ -"You could not be verified; please try again." = "Nem sikerült ellenőrizni; próbálja meg újra."; +"You commit to:\n- Only legal content in public groups\n- Respect other users - no spam" = "Ön kijelenti, hogy:\n- nyilvános csoportokban kizárólag megengedett tartalmakat oszt meg\n- tiszteletben tartja a többi felhasználót – nem küld senkinek kéretlen tartalmat"; /* No comment provided by engineer. */ -"You decide who can connect." = "Ön dönti el, hogy kivel beszélget."; +"You connected to the channel via this relay link." = "Ön ezen az átjátszóhivatkozáson keresztül kapcsolódott a csatornához."; + +/* No comment provided by engineer. */ +"You could not be verified; please try again." = "Nem sikerült ellenőrizni; próbálja meg újra."; /* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Ön már küldött egy kapcsolódási kérést!\nMegismétli a kapcsolódási kérést?"; @@ -6289,6 +6899,9 @@ report reason */ /* snd group event chat item */ "you unblocked %@" = "Ön feloldotta %@ letiltását"; +/* No comment provided by engineer. */ +"You were born without an account" = "Fiók nélkül születtünk."; + /* No comment provided by engineer. */ "You will be able to send messages **only after your request is accepted**." = "Csak azután tud üzeneteket küldeni, **miután a kérését elfogadták**."; @@ -6310,6 +6923,9 @@ report reason */ /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak."; +/* No comment provided by engineer. */ +"You will stop receiving messages from this channel. Chat history will be preserved." = "Ön nem fog több üzenetet kapni ebből a csatornából. A csevegési előzmények megmaradnak."; + /* No comment provided by engineer. */ "You will stop receiving messages from this chat. Chat history will be preserved." = "Nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak."; @@ -6334,6 +6950,9 @@ report reason */ /* No comment provided by engineer. */ "Your calls" = "Hívások"; +/* No comment provided by engineer. */ +"Your channel" = "Saját csatorna"; + /* No comment provided by engineer. */ "Your chat database" = "Csevegési adatbázis"; @@ -6364,6 +6983,9 @@ report reason */ /* No comment provided by engineer. */ "Your contacts will remain connected." = "A partnereivel továbbra is kapcsolatban marad."; +/* No comment provided by engineer. */ +"Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public." = "A beszélgetései Önhöz tartoznak, ahogy az internet megjelenése előtt is mindig így volt. A hálózat nem egy hely, amelyet meglátogat. Ez egy olyan hely, amelyet Ön hoz létre saját magának. És senki sem veheti el Öntől, függetlenül attól, hogy privát vagy nyilvános."; + /* No comment provided by engineer. */ "Your credentials may be sent unencrypted." = "A hitelesítési adatai titkosítatlanul is elküldhetők."; @@ -6379,6 +7001,9 @@ report reason */ /* No comment provided by engineer. */ "Your ICE servers" = "Saját ICE-kiszolgálók"; +/* No comment provided by engineer. */ +"Your network" = "Saját hálózat"; + /* No comment provided by engineer. */ "Your preferences" = "Beállítások"; @@ -6388,6 +7013,9 @@ report reason */ /* No comment provided by engineer. */ "Your profile" = "Saját profil"; +/* No comment provided by engineer. */ +"Your profile **%@** will be shared with channel relays and subscribers.\nRelays can access channel messages." = "A(z) **%@** nevű profilja meg lesz osztva a csatorna átjátszóival és feliratkozóival.\nAz átjátszók hozzáférhetnek a csatornaüzenetekhez."; + /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "A(z) **%@** nevű profilja meg lesz osztva."; @@ -6400,11 +7028,20 @@ report reason */ /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "A profilja módosult. Ha menti, akkor a profilfrissítés el lesz küldve a partnerei számára."; +/* No comment provided by engineer. */ +"Your public address" = "Saját nyilvános cím"; + /* No comment provided by engineer. */ "Your random profile" = "Véletlenszerű profil"; /* No comment provided by engineer. */ -"Your server address" = "Saját SMP-kiszolgálójának címe"; +"Your relay address" = "Saját átjátszó címe"; + +/* No comment provided by engineer. */ +"Your relay name" = "Saját átjátszó neve"; + +/* No comment provided by engineer. */ +"Your server address" = "Saját SMP-kiszolgáló címe"; /* No comment provided by engineer. */ "Your servers" = "Saját kiszolgálók"; diff --git a/apps/ios/hu.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/hu.lproj/SimpleX--iOS--InfoPlist.strings index 8b56c51595..d1b68ad52c 100644 --- a/apps/ios/hu.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/hu.lproj/SimpleX--iOS--InfoPlist.strings @@ -2,7 +2,7 @@ "CFBundleName" = "SimpleX"; /* Privacy - Camera Usage Description */ -"NSCameraUsageDescription" = "A SimpleXnek kamera-hozzáférésre van szüksége a QR-kódok beolvasásához, hogy kapcsolódhasson más felhasználókhoz és videohívásokhoz."; +"NSCameraUsageDescription" = "A SimpleXnek hozzáférésre van szüksége a kamerához a QR-kódok beolvasásához, hogy kapcsolódhasson más felhasználókhoz és videohívásokhoz."; /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "A SimpleX Face ID-t használ a helyi hitelesítéshez"; @@ -11,7 +11,7 @@ "NSLocalNetworkUsageDescription" = "A SimpleX helyi hálózati hozzáférést használ, hogy lehetővé tegye a felhasználói csevegési profil használatát számítógépen keresztül ugyanazon a hálózaton."; /* Privacy - Microphone Usage Description */ -"NSMicrophoneUsageDescription" = "A SimpleXnek mikrofon-hozzáférésre van szüksége hang- és videohívásokhoz, valamint hangüzenetek rögzítéséhez."; +"NSMicrophoneUsageDescription" = "A SimpleXnek hozzáférésre van szüksége a mikrofonhoz a hang- és videohívásokhoz, valamint hangüzenetek rögzítéséhez."; /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "A SimpleXnek hozzáférésre van szüksége a galériához a rögzített és fogadott média mentéséhez"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 3955f267ce..96b117eeca 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -10,6 +10,9 @@ /* 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!"; +/* No comment provided by engineer. */ +"- opt-in to send link previews.\n- prevent hyperlink phishing.\n- remove link tracking." = "- scegli se inviare anteprime dei link.\n- previeni il phishing dei collegamenti ipertestuali.\n- rimuovi il tracciamento dei link."; + /* No comment provided by engineer. */ "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- avvisa facoltativamente i contatti eliminati.\n- nomi del profilo con spazi.\n- e molto altro!"; @@ -19,21 +22,21 @@ /* No comment provided by engineer. */ "!1 colored!" = "!1 colorato!"; +/* chat link info line */ +"(from owner)" = "(dal proprietario)"; + /* No comment provided by engineer. */ "(new)" = "(nuovo)"; +/* chat link info line */ +"(signed)" = "(firmato)"; + /* No comment provided by engineer. */ "(this device v%@)" = "(questo dispositivo v%@)"; -/* No comment provided by engineer. */ -"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Contribuisci](https://github.com/simplex-chat/simplex-chat#contribute)"; - /* No comment provided by engineer. */ "[Send us email](mailto:chat@simplex.chat)" = "[Inviaci un'email](mailto:chat@simplex.chat)"; -/* No comment provided by engineer. */ -"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Dai una stella su GitHub](https://github.com/simplex-chat/simplex-chat)"; - /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Aggiungi contatto**: per creare un nuovo link di invito."; @@ -64,6 +67,9 @@ /* No comment provided by engineer. */ "**Scan / Paste link**: to connect via a link you received." = "**Scansiona / Incolla link**: per connetterti tramite un link che hai ricevuto."; +/* No comment provided by engineer. */ +"**Test relay** to retrieve its name." = "**Prova il relay** per recuperare il suo nome."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Attenzione**: le notifiche push istantanee richiedono una password salvata nel portachiavi."; @@ -175,6 +181,18 @@ /* time interval */ "%d months" = "%d mesi"; +/* channel relay bar +channel subscriber relay bar */ +"%d relays failed" = "%d relay falliti"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays not active" = "%d relay non attivi"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays removed" = "%d relay rimossi"; + /* time interval */ "%d sec" = "%d sec"; @@ -184,15 +202,50 @@ /* integrity error chat item */ "%d skipped message(s)" = "%d messaggio/i saltato/i"; +/* channel subscriber count */ +"%d subscriber" = "%d iscritto"; + +/* channel subscriber count */ +"%d subscribers" = "%d iscritti"; + /* time interval */ "%d weeks" = "%d settimane"; +/* channel creation progress +channel relay bar progress */ +"%d/%d relays active" = "%1$d/%2$d relay attivo/i"; + +/* channel relay bar */ +"%d/%d relays active, %d errors" = "%1$d/%2$d relay attivi, %3$d errori"; + +/* channel creation progress with errors +channel relay bar */ +"%d/%d relays active, %d failed" = "%1$d/%2$d relay attivo/i, %3$d fallito/i"; + +/* channel relay bar */ +"%d/%d relays active, %d removed" = "%1$d/%2$d relay attivi, %3$d rimossi"; + +/* channel subscriber relay bar progress */ +"%d/%d relays connected" = "%1$d/%2$d relay connesso/i"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d errors" = "%1$d/%2$d relay connesso/i, %3$d errori"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d failed" = "%1$d/%2$d relay connessi, %3$d falliti"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d removed" = "%1$d/%2$d relay connessi, %3$d rimossi"; + /* No comment provided by engineer. */ "%lld" = "%lld"; /* No comment provided by engineer. */ "%lld %@" = "%lld %@"; +/* No comment provided by engineer. */ +"%lld channel events" = "%lld eventi del canale"; + /* No comment provided by engineer. */ "%lld contact(s) selected" = "%lld contatto/i selezionato/i"; @@ -262,6 +315,9 @@ /* No comment provided by engineer. */ "~strike~" = "\\~barrato~"; +/* owner verification */ +"⚠️ Signature verification failed: %@." = "⚠️ Verifica della firma fallita: %@."; + /* time to disappear */ "0 sec" = "0 sec"; @@ -307,6 +363,9 @@ time interval */ /* No comment provided by engineer. */ "A few more things" = "Qualche altra cosa"; +/* No comment provided by engineer. */ +"A link for one person to connect" = "Un link per una persona da connettere"; + /* notification title */ "A new contact" = "Un contatto nuovo"; @@ -371,6 +430,9 @@ swipe action */ /* alert title */ "Accept member" = "Accetta membro"; +/* No comment provided by engineer. */ +"accepted" = "accettato"; + /* rcv group event chat item */ "accepted %@" = "%@ accettato"; @@ -392,6 +454,9 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledgement errors" = "Errori di riconoscimento"; +/* No comment provided by engineer. */ +"active" = "attivo"; + /* token status text */ "Active" = "Attivo"; @@ -399,7 +464,7 @@ swipe action */ "Active connections" = "Connessioni attive"; /* 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." = "Aggiungi l'indirizzo al tuo profilo, in modo che i tuoi contatti possano condividerlo con altre persone. L'aggiornamento del profilo verrà inviato ai tuoi contatti."; +"Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts." = "Aggiungi l'indirizzo al tuo profilo, in modo che i tuoi contatti di SimpleX possano condividerlo con altre persone. L'aggiornamento del profilo verrà inviato ai tuoi contatti di SimpleX."; /* No comment provided by engineer. */ "Add friends" = "Aggiungi amici"; @@ -440,6 +505,9 @@ swipe action */ /* No comment provided by engineer. */ "Added message servers" = "Server dei messaggi aggiunti"; +/* No comment provided by engineer. */ +"Adding relays will be supported later." = "L'aggiunta di relay verrà supportata prossimamente."; + /* No comment provided by engineer. */ "Additional accent" = "Principale aggiuntivo"; @@ -530,6 +598,12 @@ swipe action */ /* profile dropdown */ "All profiles" = "Tutti gli profili"; +/* No comment provided by engineer. */ +"All relays failed" = "Tutti i relay falliti"; + +/* No comment provided by engineer. */ +"All relays removed" = "Tutti i relay rimossi"; + /* No comment provided by engineer. */ "All reports will be archived for you." = "Tutte le segnalazioni verranno archiviate per te."; @@ -566,6 +640,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Consenti l'eliminazione irreversibile dei messaggi solo se il contatto la consente a te. (24 ore)"; +/* No comment provided by engineer. */ +"Allow members to chat with admins." = "Consenti ai membri di chattare con gli amministratori."; + /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "Consenti reazioni ai messaggi solo se il tuo contatto le consente."; @@ -575,12 +652,18 @@ swipe action */ /* No comment provided by engineer. */ "Allow sending direct messages to members." = "Permetti l'invio di messaggi diretti ai membri."; +/* No comment provided by engineer. */ +"Allow sending direct messages to subscribers." = "Permetti l'invio di messaggi diretti agli iscritti."; + /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Permetti l'invio di messaggi a tempo."; /* No comment provided by engineer. */ "Allow sharing" = "Consenti la condivisione"; +/* No comment provided by engineer. */ +"Allow subscribers to chat with admins." = "Consenti agli iscritti di chattare con gli amministratori."; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Permetti di eliminare irreversibilmente i messaggi inviati. (24 ore)"; @@ -650,9 +733,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Rispondi alla chiamata"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "Chiunque può installare i server."; - /* No comment provided by engineer. */ "App build: %@" = "Build dell'app: %@"; @@ -771,7 +851,7 @@ swipe action */ "Auto-accept contact requests" = "Auto-accetta le richieste di contatto"; /* No comment provided by engineer. */ -"Auto-accept images" = "Auto-accetta le immagini"; +"Auto-accept images" = "Accetta automaticamente le immagini"; /* No comment provided by engineer. */ "Back" = "Indietro"; @@ -794,6 +874,15 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "ID del messaggio errato"; +/* No comment provided by engineer. */ +"Be free\nin your network" = "Vivi libero\nnella tua rete"; + +/* No comment provided by engineer. */ +"Be free in your network." = "Vivi libero nella tua rete."; + +/* No comment provided by engineer. */ +"Because we destroyed the power to know who you are. So that your power can never be taken." = "Perché abbiamo distrutto il potere di sapere chi sei. In modo che il tuo potere non possa mai esserti sottratto."; + /* No comment provided by engineer. */ "Better calls" = "Chiamate migliorate"; @@ -851,6 +940,9 @@ swipe action */ /* No comment provided by engineer. */ "Block member?" = "Bloccare il membro?"; +/* No comment provided by engineer. */ +"Block subscriber for all?" = "Bloccare l'iscritto per tutti?"; + /* marked deleted chat item preview text */ "blocked" = "bloccato"; @@ -895,9 +987,15 @@ marked deleted chat item preview text */ "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)!"; +"Bottom bar" = "Barra inferiore"; + +/* compose placeholder for channel owner */ +"Broadcast" = "Trasmetti"; /* 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)!"; + +/* chat link info line */ "Business address" = "Indirizzo di lavoro"; /* No comment provided by engineer. */ @@ -912,9 +1010,6 @@ marked deleted chat item preview text */ /* 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)."; -/* No comment provided by engineer. */ -"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "Usando SimpleX Chat accetti di:\n- inviare solo contenuto legale nei gruppi pubblici.\n- rispettare gli altri utenti - niente spam."; - /* No comment provided by engineer. */ "call" = "chiama"; @@ -939,6 +1034,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Camera not available" = "Fotocamera non disponibile"; +/* No comment provided by engineer. */ +"can't broadcast" = "impossibile trasmettere"; + /* No comment provided by engineer. */ "Can't call contact" = "Impossibile chiamare il contatto"; @@ -1038,6 +1136,58 @@ set passcode view */ /* chat item text */ "changing address…" = "cambio indirizzo…"; +/* shown as sender role for channel messages */ +"channel" = "canale"; + +/* No comment provided by engineer. */ +"Channel" = "Canale"; + +/* No comment provided by engineer. */ +"Channel display name" = "Nome da mostrare del canale"; + +/* No comment provided by engineer. */ +"Channel full name (optional)" = "Nome completo del canale (facoltativo)"; + +/* alert message +alert subtitle */ +"Channel has no active relays. Please try to join later." = "Il canale non ha relay attivi. Prova a iscriverti più tardi."; + +/* No comment provided by engineer. */ +"Channel image" = "Immagine del canale"; + +/* chat link info line */ +"Channel link" = "Link del canale"; + +/* No comment provided by engineer. */ +"Channel preferences" = "Preferenze del canale"; + +/* No comment provided by engineer. */ +"Channel profile" = "Profilo del canale"; + +/* No comment provided by engineer. */ +"Channel profile is stored on subscribers' devices and on the chat relays." = "Il profilo del canale è memorizzato sui dispositivi degli iscritti e sui relay di chat."; + +/* snd group event chat item */ +"channel profile updated" = "profilo del canale aggiornato"; + +/* alert message */ +"Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers." = "Il profilo del canale è stato cambiato. Se lo salvi, il profilo aggiornato verrà inviato agli iscritti di canale."; + +/* alert title */ +"Channel temporarily unavailable" = "Canale non disponibile temporaneamente"; + +/* No comment provided by engineer. */ +"Channel will be deleted for all subscribers - this cannot be undone!" = "Il canale verrà eliminato per tutti gli iscritti, non è reversibile!"; + +/* No comment provided by engineer. */ +"Channel will be deleted for you - this cannot be undone!" = "Il canale verrà eliminato per te, non è reversibile!"; + +/* alert message */ +"Channel will start working with %d of %d relays. Proceed?" = "Il canale sarà operativo con %1$d di %2$d relay. Procedere?"; + +/* No comment provided by engineer. */ +"Channels" = "Canali"; + /* No comment provided by engineer. */ "Chat" = "Chat"; @@ -1089,6 +1239,18 @@ set passcode view */ /* No comment provided by engineer. */ "Chat profile" = "Profilo utente"; +/* No comment provided by engineer. */ +"Chat relay" = "Relay di chat"; + +/* No comment provided by engineer. */ +"Chat relays" = "Relay di chat"; + +/* No comment provided by engineer. */ +"Chat relays forward messages in channels you create." = "I relay di chat inoltrano i messaggi nei canali che crei."; + +/* No comment provided by engineer. */ +"Chat relays forward messages to channel subscribers." = "I relay di chat inoltrano i messaggi agli iscritti del canale."; + /* No comment provided by engineer. */ "Chat theme" = "Tema della chat"; @@ -1098,7 +1260,8 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "La chat verrà eliminata solo per te, non è reversibile!"; -/* chat toolbar */ +/* chat feature +chat toolbar */ "Chat with admins" = "Chat con amministratori"; /* No comment provided by engineer. */ @@ -1110,15 +1273,30 @@ set passcode view */ /* No comment provided by engineer. */ "Chats" = "Chat"; +/* No comment provided by engineer. */ +"Chats with admins are prohibited." = "Le chat con gli amministratori sono vietate."; + +/* alert message */ +"Chats with admins in public channels have no E2E encryption - use only with trusted chat relays." = "Le chat con amministratori in canali pubblici non hanno crittografia E2E: usale solo con relay di chat fidati."; + /* No comment provided by engineer. */ "Chats with members" = "Chat con membri"; +/* No comment provided by engineer. */ +"Chats with members are disabled" = "Le chat con i membri sono disattivate"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Controlla i messaggi ogni 20 min."; /* No comment provided by engineer. */ "Check messages when allowed." = "Controlla i messaggi quando consentito."; +/* alert message */ +"Check relay address and try again." = "Controlla l'indirizzo del relay e riprova."; + +/* alert message */ +"Check relay name and try again." = "Controlla il nome del relay e riprova."; + /* alert title */ "Check server address and try again." = "Controlla l'indirizzo del server e riprova."; @@ -1213,7 +1391,7 @@ set passcode view */ "Configure ICE servers" = "Configura server ICE"; /* No comment provided by engineer. */ -"Configure server operators" = "Configura gli operatori dei server"; +"Configure relays" = "Configura i relay"; /* No comment provided by engineer. */ "Confirm" = "Conferma"; @@ -1248,7 +1426,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Confermato"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Connetti"; /* No comment provided by engineer. */ @@ -1278,6 +1457,9 @@ set passcode view */ /* new chat sheet title */ "Connect via link" = "Connetti via link"; +/* No comment provided by engineer. */ +"Connect via link or QR code" = "Connetti via link o codice QR"; + /* new chat sheet title */ "Connect via one-time link" = "Connetti via link una tantum"; @@ -1347,12 +1529,15 @@ set passcode view */ /* alert title */ "Connection error" = "Errore di connessione"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "Errore di connessione (AUTH)"; /* chat list item title (it should not be shown */ "connection established" = "connessione stabilita"; +/* No comment provided by engineer. */ +"Connection failed" = "Connessione fallita"; + /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "La connessione è bloccata dall'operatore del server:\n%@"; @@ -1389,6 +1574,9 @@ set passcode view */ /* profile update event chat item */ "contact %@ changed to %@" = "contatto %1$@ cambiato in %2$@"; +/* chat link info line */ +"Contact address" = "Indirizzo di contatto"; + /* No comment provided by engineer. */ "Contact allows" = "Il contatto lo consente"; @@ -1449,6 +1637,9 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "Continua"; +/* No comment provided by engineer. */ +"Contribute" = "Contribuisci"; + /* No comment provided by engineer. */ "Conversation deleted!" = "Conversazione eliminata!"; @@ -1464,12 +1655,9 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Angolo"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Correggere il nome a %@?"; -/* No comment provided by engineer. */ -"Create" = "Crea"; - /* No comment provided by engineer. */ "Create 1-time link" = "Crea link una tantum"; @@ -1497,6 +1685,12 @@ set passcode view */ /* No comment provided by engineer. */ "Create profile" = "Crea profilo"; +/* No comment provided by engineer. */ +"Create public channel" = "Crea canale pubblico"; + +/* No comment provided by engineer. */ +"Create public channel (BETA)" = "Crea canale pubblico (BETA)"; + /* server test step */ "Create queue" = "Crea coda"; @@ -1506,9 +1700,15 @@ set passcode view */ /* No comment provided by engineer. */ "Create your address" = "Crea il tuo indirizzo"; +/* No comment provided by engineer. */ +"Create your link" = "Connettiti con qualcuno"; + /* No comment provided by engineer. */ "Create your profile" = "Crea il tuo profilo"; +/* No comment provided by engineer. */ +"Create your public address" = "Crea il tuo indirizzo pubblico"; + /* No comment provided by engineer. */ "Created" = "Creato"; @@ -1521,6 +1721,9 @@ set passcode view */ /* No comment provided by engineer. */ "Creating archive link" = "Creazione link dell'archivio"; +/* No comment provided by engineer. */ +"Creating channel" = "Creazione canale"; + /* No comment provided by engineer. */ "Creating link…" = "Creazione link…"; @@ -1623,8 +1826,8 @@ set passcode view */ /* No comment provided by engineer. */ "Debug delivery" = "Debug della consegna"; -/* No comment provided by engineer. */ -"Decentralized" = "Decentralizzato"; +/* relay test step */ +"Decode link" = "Decodifica il link"; /* message decrypt error item */ "Decryption error" = "Errore di decifrazione"; @@ -1667,6 +1870,12 @@ swipe action */ /* No comment provided by engineer. */ "Delete and notify contact" = "Elimina e avvisa il contatto"; +/* No comment provided by engineer. */ +"Delete channel" = "Elimina canale"; + +/* No comment provided by engineer. */ +"Delete channel?" = "Eliminare il canale?"; + /* No comment provided by engineer. */ "Delete chat" = "Elimina chat"; @@ -1770,6 +1979,9 @@ alert button */ /* server test step */ "Delete queue" = "Elimina coda"; +/* No comment provided by engineer. */ +"Delete relay" = "Elimina relay"; + /* No comment provided by engineer. */ "Delete report" = "Elimina la segnalazione"; @@ -1794,6 +2006,9 @@ alert button */ /* copied message info */ "Deleted at: %@" = "Eliminato il: %@"; +/* rcv group event chat item */ +"deleted channel" = "canale eliminato"; + /* rcv direct event chat item */ "deleted contact" = "contatto eliminato"; @@ -1884,6 +2099,12 @@ alert button */ /* No comment provided by engineer. */ "Direct messages between members are prohibited." = "I messaggi diretti tra i membri sono vietati in questo gruppo."; +/* No comment provided by engineer. */ +"Direct messages between subscribers are prohibited." = "I messaggi diretti tra gli iscritti sono vietati."; + +/* alert button */ +"Disable" = "Disattiva"; + /* No comment provided by engineer. */ "Disable (keep overrides)" = "Disattiva (mantieni sostituzioni)"; @@ -1941,6 +2162,9 @@ alert button */ /* No comment provided by engineer. */ "Do not send history to new members." = "Non inviare la cronologia ai nuovi membri."; +/* No comment provided by engineer. */ +"Do not send history to new subscribers." = "Non inviare la cronologia ai nuovi iscritti."; + /* No comment provided by engineer. */ "Do NOT send messages directly, even if your or destination server does not support private routing." = "NON inviare messaggi direttamente, anche se il tuo server o quello di destinazione non supporta l'instradamento privato."; @@ -2020,27 +2244,39 @@ chat item action */ /* No comment provided by engineer. */ "E2E encrypted notifications." = "Notifiche crittografate E2E."; +/* No comment provided by engineer. */ +"Easier to invite your friends 👋" = "È più facile invitare i tuoi amici 👋"; + /* chat item action */ "Edit" = "Modifica"; +/* No comment provided by engineer. */ +"Edit channel profile" = "Modifica profilo canale"; + /* No comment provided by engineer. */ "Edit group profile" = "Modifica il profilo del gruppo"; /* No comment provided by engineer. */ "Empty message!" = "Messaggio vuoto!"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "Attiva"; /* No comment provided by engineer. */ "Enable (keep overrides)" = "Attiva (mantieni sostituzioni)"; +/* channel creation warning */ +"Enable at least one chat relay in Network & Servers." = "Attiva almeno un relay di chat in \"Rete e server\"."; + /* alert title */ "Enable automatic message deletion?" = "Attivare l'eliminazione automatica dei messaggi?"; /* No comment provided by engineer. */ "Enable camera access" = "Attiva l'accesso alla fotocamera"; +/* alert title */ +"Enable chats with admins?" = "Attivare le chat con gli amministratori?"; + /* No comment provided by engineer. */ "Enable disappearing messages by default." = "Attiva i messaggi a tempo in modo predefinito."; @@ -2056,11 +2292,11 @@ chat item action */ /* No comment provided by engineer. */ "Enable instant notifications?" = "Attivare le notifiche istantanee?"; -/* No comment provided by engineer. */ -"Enable lock" = "Attiva blocco"; +/* alert title */ +"Enable link previews?" = "Attivare le anteprime dei link?"; /* No comment provided by engineer. */ -"Enable notifications" = "Attiva le notifiche"; +"Enable lock" = "Attiva blocco"; /* No comment provided by engineer. */ "Enable periodic notifications?" = "Attivare le notifiche periodiche?"; @@ -2167,6 +2403,9 @@ chat item action */ /* call status */ "ended call %@" = "chiamata terminata %@"; +/* No comment provided by engineer. */ +"Enter channel name…" = "Inserisci il nome del canale…"; + /* No comment provided by engineer. */ "Enter correct passphrase." = "Inserisci la password giusta."; @@ -2185,6 +2424,12 @@ chat item action */ /* No comment provided by engineer. */ "Enter password above to show!" = "Inserisci la password sopra per mostrare!"; +/* No comment provided by engineer. */ +"Enter profile name..." = "Inserisci nome profilo..."; + +/* No comment provided by engineer. */ +"Enter relay name…" = "Inserisci il nome del relay…"; + /* No comment provided by engineer. */ "Enter server manually" = "Inserisci il server a mano"; @@ -2203,7 +2448,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "errore"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "Errore"; /* No comment provided by engineer. */ @@ -2221,6 +2466,9 @@ chat item action */ /* No comment provided by engineer. */ "Error adding member(s)" = "Errore di aggiunta membro/i"; +/* alert title */ +"Error adding relay" = "Errore di aggiunta del relay"; + /* alert title */ "Error adding server" = "Errore di aggiunta del server"; @@ -2257,6 +2505,9 @@ chat item action */ /* No comment provided by engineer. */ "Error creating address" = "Errore nella creazione dell'indirizzo"; +/* alert title */ +"Error creating channel" = "Errore di creazione del canale"; + /* No comment provided by engineer. */ "Error creating group" = "Errore nella creazione del gruppo"; @@ -2338,9 +2589,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Errore di apertura della chat"; -/* No comment provided by engineer. */ -"Error opening group" = "Errore di preparazione del gruppo"; - /* alert title */ "Error receiving file" = "Errore nella ricezione del file"; @@ -2365,6 +2613,9 @@ chat item action */ /* No comment provided by engineer. */ "Error resetting statistics" = "Errore di azzeramento statistiche"; +/* No comment provided by engineer. */ +"Error saving channel profile" = "Errore di salvataggio del profilo del canale"; + /* alert title */ "Error saving chat list" = "Errore nel salvataggio dell'elenco di chat"; @@ -2407,6 +2658,9 @@ chat item action */ /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Errore nell'impostazione delle ricevute di consegna!"; +/* alert title */ +"Error sharing channel" = "Errore nella condivisione del canale"; + /* No comment provided by engineer. */ "Error starting chat" = "Errore di avvio della chat"; @@ -2449,12 +2703,16 @@ chat item action */ /* No comment provided by engineer. */ "Error: " = "Errore: "; +/* receive error chat item */ +"error: %@" = "errore: %@"; + /* alert message file error text snd error text */ "Error: %@" = "Errore: %@"; -/* server test error */ +/* relay test error +server test error */ "Error: %@." = "Errore: %@."; /* No comment provided by engineer. */ @@ -2502,6 +2760,9 @@ snd error text */ /* No comment provided by engineer. */ "Exporting database archive…" = "Esportazione archivio database…"; +/* No comment provided by engineer. */ +"failed" = "fallito"; + /* No comment provided by engineer. */ "Failed to remove passphrase" = "Rimozione della password fallita"; @@ -2604,7 +2865,8 @@ snd error text */ /* No comment provided by engineer. */ "Fingerprint in server address does not match certificate: %@." = "L'impronta digitale nell'indirizzo del server non corrisponde al certificato: %@."; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "L'impronta digitale nell'indirizzo del server non corrisponde al certificato."; /* No comment provided by engineer. */ @@ -2628,7 +2890,11 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Per tutti i moderatori"; -/* servers error */ +/* No comment provided by engineer. */ +"For anyone to reach you" = "Per chiunque debba raggiungerti"; + +/* servers error +servers warning */ "For chat profile %@:" = "Per il profilo di chat %@:"; /* No comment provided by engineer. */ @@ -2712,9 +2978,15 @@ snd error text */ /* No comment provided by engineer. */ "Further reduced battery usage" = "Ulteriore riduzione del consumo della batteria"; +/* relay test step */ +"Get link" = "Ottieni link"; + /* No comment provided by engineer. */ "Get notified when mentioned." = "Ricevi una notifica quando menzionato."; +/* No comment provided by engineer. */ +"Get started" = "Cominciamo"; + /* No comment provided by engineer. */ "GIFs and stickers" = "GIF e adesivi"; @@ -2760,7 +3032,7 @@ snd error text */ /* No comment provided by engineer. */ "group is deleted" = "il gruppo è eliminato"; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "Link del gruppo"; /* No comment provided by engineer. */ @@ -2832,6 +3104,9 @@ snd error text */ /* No comment provided by engineer. */ "History is not sent to new members." = "La cronologia non viene inviata ai nuovi membri."; +/* No comment provided by engineer. */ +"History is not sent to new subscribers." = "La cronologia non viene inviata ai nuovi iscritti."; + /* time unit */ "hours" = "ore"; @@ -2871,6 +3146,9 @@ snd error text */ /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "Se inserisci il tuo codice di autodistruzione mentre apri l'app:"; +/* down migration warning */ +"If you joined or created channels, they will stop working permanently." = "Se sei dentro canali o ne hai creati, essi smetteranno di funzionare definitivamente."; + /* 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)." = "Se devi usare la chat adesso, tocca **Fallo più tardi** qui sotto (ti verrà offerto di migrare il database quando riavvii l'app)."; @@ -2889,9 +3167,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "Immediatamente"; -/* No comment provided by engineer. */ -"Immune to spam" = "Immune a spam e abusi"; - /* No comment provided by engineer. */ "Import" = "Importa"; @@ -2992,7 +3267,7 @@ snd error text */ "Initial role" = "Ruolo iniziale"; /* No comment provided by engineer. */ -"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Installa [Simplex Chat per terminale](https://github.com/simplex-chat/simplex-chat)"; +"Install SimpleX Chat for terminal" = "Installa Simplex Chat per terminale"; /* No comment provided by engineer. */ "Instant" = "Istantaneamente"; @@ -3027,7 +3302,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "dati chat non validi"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "Link di connessione non valido"; /* invalid chat item */ @@ -3042,12 +3317,18 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Conferma di migrazione non valida"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Nome non valido!"; /* No comment provided by engineer. */ "Invalid QR code" = "Codice QR non valido"; +/* alert title */ +"Invalid relay address!" = "Indirizzo del relay non valido!"; + +/* alert title */ +"Invalid relay name!" = "Nome del relay non valido!"; + /* No comment provided by engineer. */ "Invalid response" = "Risposta non valida"; @@ -3075,6 +3356,9 @@ snd error text */ /* No comment provided by engineer. */ "Invite members" = "Invita membri"; +/* No comment provided by engineer. */ +"Invite someone privately" = "Invita qualcuno in modo privato"; + /* No comment provided by engineer. */ "Invite to chat" = "Invita in chat"; @@ -3141,6 +3425,9 @@ snd error text */ /* No comment provided by engineer. */ "Join as %@" = "entra come %@"; +/* No comment provided by engineer. */ +"Join channel" = "Iscriviti al canale"; + /* new chat sheet title */ "Join group" = "Entra nel gruppo"; @@ -3189,6 +3476,12 @@ snd error text */ /* swipe action */ "Leave" = "Esci"; +/* No comment provided by engineer. */ +"Leave channel" = "Esci dal canale"; + +/* No comment provided by engineer. */ +"Leave channel?" = "Uscire dal canale?"; + /* No comment provided by engineer. */ "Leave chat" = "Esci dalla chat"; @@ -3207,6 +3500,9 @@ snd error text */ /* No comment provided by engineer. */ "Less traffic on mobile networks." = "Meno traffico sulle reti mobili."; +/* No comment provided by engineer. */ +"Let someone connect to you" = "Lascia che qualcuno si connetta a te"; + /* email subject */ "Let's talk in SimpleX Chat" = "Parliamo in SimpleX Chat"; @@ -3216,9 +3512,15 @@ snd error text */ /* No comment provided by engineer. */ "Limitations" = "Limitazioni"; +/* No comment provided by engineer. */ +"link" = "link"; + /* No comment provided by engineer. */ "Link mobile and desktop apps! 🔗" = "Collega le app mobile e desktop! 🔗"; +/* owner verification */ +"Link signature verified." = "Firma del link verificata."; + /* No comment provided by engineer. */ "Linked desktop options" = "Opzioni del desktop collegato"; @@ -3348,6 +3650,9 @@ snd error text */ /* No comment provided by engineer. */ "Members can add message reactions." = "I membri del gruppo possono aggiungere reazioni ai messaggi."; +/* No comment provided by engineer. */ +"Members can chat with admins." = "I membri possono chattare con gli amministratori."; + /* No comment provided by engineer. */ "Members can irreversibly delete sent messages. (24 hours)" = "I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore)"; @@ -3390,6 +3695,9 @@ snd error text */ /* No comment provided by engineer. */ "Message draft" = "Bozza del messaggio"; +/* No comment provided by engineer. */ +"Message error" = "Errore del messaggio"; + /* item status text */ "Message forwarded" = "Messaggio inoltrato"; @@ -3450,6 +3758,12 @@ snd error text */ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "I messaggi da %@ verranno mostrati!"; +/* No comment provided by engineer. */ +"Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages." = "I messaggi in questo canale **non sono crittografati end-to-end**. I relay di chat possono vedere questi messaggi."; + +/* E2EE info chat item */ +"Messages in this channel are not end-to-end encrypted. Chat relays can see these messages." = "I messaggi in questo canale non sono crittografati end-to-end. I relay di chat possono vedere questi messaggi."; + /* alert message */ "Messages in this chat will never be deleted." = "I messaggi in questa chat non verranno mai eliminati."; @@ -3469,10 +3783,10 @@ snd error text */ "Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "I messaggi, i file e le chiamate sono protetti da **crittografia e2e resistente alla quantistica** con perfect forward secrecy, ripudio e recupero da intrusione."; /* No comment provided by engineer. */ -"Migrate device" = "Migra dispositivo"; +"Migrate" = "Migra"; /* No comment provided by engineer. */ -"Migrate from another device" = "Migra da un altro dispositivo"; +"Migrate device" = "Migra dispositivo"; /* No comment provided by engineer. */ "Migrate here" = "Migra qui"; @@ -3564,12 +3878,18 @@ snd error text */ /* No comment provided by engineer. */ "Network & servers" = "Rete e server"; +/* No comment provided by engineer. */ +"Network commitments" = "Impegni sulla rete"; + /* No comment provided by engineer. */ "Network connection" = "Connessione di rete"; /* No comment provided by engineer. */ "Network decentralization" = "Decentralizzazione della rete"; +/* conn error description */ +"Network error" = "Errore di rete"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "Problemi di rete - messaggio scaduto dopo molti tentativi di inviarlo."; @@ -3579,6 +3899,9 @@ snd error text */ /* No comment provided by engineer. */ "Network operator" = "Operatore di rete"; +/* No comment provided by engineer. */ +"Network routers cannot know\nwho talks to whom" = "Gli instradatori di rete non possono\nsapere chi parla con chi"; + /* No comment provided by engineer. */ "Network settings" = "Impostazioni di rete"; @@ -3588,15 +3911,24 @@ snd error text */ /* delete after time */ "never" = "mai"; +/* No comment provided by engineer. */ +"new" = "nuovo"; + /* token status text */ "New" = "Nuovo"; +/* No comment provided by engineer. */ +"New 1-time link" = "Nuovo link una tantum"; + /* No comment provided by engineer. */ "New chat" = "Nuova chat"; /* No comment provided by engineer. */ "New chat experience 🎉" = "Una nuova esperienza di chat 🎉"; +/* No comment provided by engineer. */ +"New chat relay" = "Nuovo relay di chat"; + /* notification */ "New contact request" = "Nuova richiesta di contatto"; @@ -3654,9 +3986,21 @@ snd error text */ /* No comment provided by engineer. */ "No" = "No"; +/* No comment provided by engineer. */ +"No account. No phone. No email. No ID.\nThe most secure encryption." = "Nessun account. Nessun telefono. Nessuna email. Nessun identificatore.\nLa crittografia più sicura."; + +/* No comment provided by engineer. */ +"No active relays" = "Nessun relay attivo"; + /* Authentication unavailable */ "No app password" = "Nessuna password dell'app"; +/* No comment provided by engineer. */ +"No chat relays" = "Nessun relay di chat"; + +/* servers warning */ +"No chat relays enabled." = "Nessun relay di chat attivato."; + /* No comment provided by engineer. */ "No chats" = "Nessuna chat"; @@ -3754,7 +4098,16 @@ snd error text */ "No unread chats" = "Nessuna chat non letta"; /* No comment provided by engineer. */ -"No user identifiers." = "Nessun identificatore utente."; +"Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "Nessuno monitorava le tue conversazioni. Nessuno disegnava una mappa delle tue posizioni. La privacy non era mai stata una caratteristica, era uno stile di vita."; + +/* No comment provided by engineer. */ +"Non-profit governance" = "Organizzazione non a scopo di lucro"; + +/* No comment provided by engineer. */ +"Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "Non una serratura migliore sulla porta di qualcun altro. Non un padrone di casa più gentile che rispetta la tua privacy, ma che continua a tenere traccia di tutti i visitatori. Non sei un ospite. Sei a casa tua. Nessun re può entrarvi: sei tu il sovrano."; + +/* alert title */ +"Not all relays connected" = "Non tutti i relay sono connessi"; /* No comment provided by engineer. */ "Not compatible!" = "Non compatibile!"; @@ -3812,7 +4165,7 @@ alert button new chat action */ "Ok" = "Ok"; -/* No comment provided by engineer. */ +/* alert button */ "OK" = "OK"; /* No comment provided by engineer. */ @@ -3821,9 +4174,15 @@ new chat action */ /* group pref value */ "on" = "on"; +/* No comment provided by engineer. */ +"On your phone, not on servers." = "Sul tuo telefono, non sui server."; + /* No comment provided by engineer. */ "One-time invitation link" = "Link di invito una tantum"; +/* chat link info line */ +"One-time link" = "Link una tantum"; + /* No comment provided by engineer. */ "Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Gli host Onion saranno **necessari** per la connessione.\nRichiede l'attivazione della VPN."; @@ -3833,6 +4192,9 @@ new chat action */ /* No comment provided by engineer. */ "Onion hosts will not be used." = "Gli host Onion non verranno usati."; +/* No comment provided by engineer. */ +"Only channel owners can change channel preferences." = "Solo i proprietari del canale possono modificarne le preferenze."; + /* No comment provided by engineer. */ "Only chat owners can change preferences." = "Solo i proprietari della chat possono modificarne le preferenze."; @@ -3893,12 +4255,16 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Solo il tuo contatto può inviare messaggi vocali."; -/* alert action */ +/* alert action +alert button */ "Open" = "Apri"; /* No comment provided by engineer. */ "Open changes" = "Apri le modifiche"; +/* new chat action */ +"Open channel" = "Apri canale"; + /* new chat action */ "Open chat" = "Apri chat"; @@ -3911,6 +4277,9 @@ new chat action */ /* No comment provided by engineer. */ "Open conditions" = "Apri le condizioni"; +/* alert title */ +"Open external link?" = "Aprire il link esterno?"; + /* alert action */ "Open full link" = "Apri link completo"; @@ -3923,6 +4292,9 @@ new chat action */ /* authentication reason */ "Open migration to another device" = "Apri migrazione ad un altro dispositivo"; +/* new chat action */ +"Open new channel" = "Apri un canale nuovo"; + /* new chat action */ "Open new chat" = "Apri una chat nuova"; @@ -3954,10 +4326,13 @@ new chat action */ "Operator server" = "Server dell'operatore"; /* No comment provided by engineer. */ -"Or import archive file" = "O importa file archivio"; +"Operators commit to:\n- Be independent\n- Minimize metadata usage\n- Run verified open-source code" = "Gli operatori si impegnano a:\n- Essere indipendenti\n- Minimizzare l'uso di metadati\n- Eseguire codice open source verificato"; /* No comment provided by engineer. */ -"Or paste archive link" = "O incolla il link dell'archivio"; +"Or import archive file" = "O importa un file dell'archivio"; + +/* No comment provided by engineer. */ +"Or paste archive link" = "O incolla un link dell'archivio"; /* No comment provided by engineer. */ "Or scan QR code" = "O scansiona il codice QR"; @@ -3965,12 +4340,18 @@ new chat action */ /* No comment provided by engineer. */ "Or securely share this file link" = "O condividi in modo sicuro questo link del file"; +/* No comment provided by engineer. */ +"Or show QR in person or via video call." = "O mostra il QR di persona o via videochiamata."; + /* No comment provided by engineer. */ "Or show this code" = "O mostra questo codice"; /* No comment provided by engineer. */ "Or to share privately" = "O per condividere in modo privato"; +/* No comment provided by engineer. */ +"Or use this QR - print or show online." = "O usa questo QR: stampalo o mostralo online."; + /* No comment provided by engineer. */ "Organize chats into lists" = "Organizza le chat in elenchi"; @@ -3989,9 +4370,18 @@ new chat action */ /* member role */ "owner" = "proprietario"; +/* No comment provided by engineer. */ +"Owner" = "Proprietario"; + /* feature role */ "owners" = "proprietari"; +/* No comment provided by engineer. */ +"Owners" = "Proprietari"; + +/* No comment provided by engineer. */ +"Ownership: you can run your own relays." = "Proprietà: puoi gestire i tuoi relay personali."; + /* No comment provided by engineer. */ "Passcode" = "Codice di accesso"; @@ -4019,6 +4409,9 @@ new chat action */ /* No comment provided by engineer. */ "Paste image" = "Incolla immagine"; +/* No comment provided by engineer. */ +"Paste link / Scan" = "Incolla link / Scansiona"; + /* No comment provided by engineer. */ "Paste link to connect!" = "Incolla un link per connettere!"; @@ -4127,6 +4520,12 @@ new chat action */ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserva la bozza dell'ultimo messaggio, con gli allegati."; +/* No comment provided by engineer. */ +"Preset relay address" = "Indirizzo relay preimpostato"; + +/* No comment provided by engineer. */ +"Preset relay name" = "Nome relay preimpostato"; + /* No comment provided by engineer. */ "Preset server address" = "Indirizzo server preimpostato"; @@ -4149,10 +4548,10 @@ new chat action */ "Privacy policy and conditions of use." = "Informativa sulla privacy e condizioni d'uso."; /* No comment provided by engineer. */ -"Privacy redefined" = "Privacy ridefinita"; +"Privacy: for owners and subscribers." = "Privacy: per i proprietari e gli iscritti."; /* No comment provided by engineer. */ -"Private chats, groups and your contacts are not accessible to server operators." = "Le chat private, i gruppi e i tuoi contatti non sono accessibili agli operatori dei server."; +"Private and secure messaging." = "Messaggistica privata e sicura."; /* No comment provided by engineer. */ "Private filenames" = "Nomi di file privati"; @@ -4178,6 +4577,9 @@ new chat action */ /* alert title */ "Private routing timeout" = "Scadenza dell'instradamento privato"; +/* alert action */ +"Proceed" = "Procedi"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profilo e connessioni al server"; @@ -4194,11 +4596,14 @@ new chat action */ "Profile theme" = "Tema del profilo"; /* alert message */ -"Profile update will be sent to your contacts." = "L'aggiornamento del profilo verrà inviato ai tuoi contatti."; +"Profile update will be sent to your SimpleX contacts." = "L'aggiornamento del profilo verrà inviato ai tuoi contatti di SimpleX."; /* No comment provided by engineer. */ "Prohibit audio/video calls." = "Proibisci le chiamate audio/video."; +/* No comment provided by engineer. */ +"Prohibit chats with admins." = "Vieta le chat con gli amministratori."; + /* No comment provided by engineer. */ "Prohibit irreversible message deletion." = "Proibisci l'eliminazione irreversibile dei messaggi."; @@ -4214,6 +4619,9 @@ new chat action */ /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "Proibisci l'invio di messaggi diretti ai membri."; +/* No comment provided by engineer. */ +"Prohibit sending direct messages to subscribers." = "Proibisci l'invio di messaggi diretti agli iscritti."; + /* No comment provided by engineer. */ "Prohibit sending disappearing messages." = "Proibisci l'invio di messaggi a tempo."; @@ -4256,6 +4664,9 @@ new chat action */ /* No comment provided by engineer. */ "Proxy requires password" = "Il proxy richiede una password"; +/* No comment provided by engineer. */ +"Public channels - speak freely 🚀" = "Canali pubblici - parla liberamente 🚀"; + /* No comment provided by engineer. */ "Push notifications" = "Notifiche push"; @@ -4284,16 +4695,10 @@ new chat action */ "Read more" = "Leggi tutto"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Leggi di più nella [Guida utente](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in our GitHub repository." = "Maggiori informazioni nel nostro repository GitHub."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; - -/* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Maggiori informazioni nella [Guida per l'utente](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)." = "Maggiori informazioni nel nostro [repository GitHub](https://github.com/simplex-chat/simplex-chat#readme)."; +"Read more in User Guide." = "Leggi di più nella Guida utente."; /* No comment provided by engineer. */ "Receipts are disabled" = "Le ricevute sono disattivate"; @@ -4402,12 +4807,36 @@ swipe action */ /* call status */ "rejected call" = "chiamata rifiutata"; +/* member role */ +"relay" = "relay"; + +/* No comment provided by engineer. */ +"Relay" = "Relay"; + +/* alert title */ +"Relay address" = "Indirizzo del relay"; + +/* alert title */ +"Relay connection failed" = "Connessione del relay fallita"; + +/* No comment provided by engineer. */ +"Relay link" = "Link del relay"; + +/* alert message */ +"Relay results:" = "Risultati relay:"; + /* No comment provided by engineer. */ "Relay server is only used if necessary. Another party can observe your IP address." = "Il server relay viene usato solo se necessario. Un altro utente può osservare il tuo indirizzo IP."; /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "Il server relay protegge il tuo indirizzo IP, ma può osservare la durata della chiamata."; +/* No comment provided by engineer. */ +"Relay test failed!" = "Prova del relay fallita!"; + +/* No comment provided by engineer. */ +"Reliability: many relays per channel." = "Affidabilità: relay multipli per canale."; + /* alert action */ "Remove" = "Rimuovi"; @@ -4432,12 +4861,24 @@ swipe action */ /* No comment provided by engineer. */ "Remove passphrase from keychain?" = "Rimuovere la password dal portachiavi?"; +/* No comment provided by engineer. */ +"Remove subscriber" = "Rimuovi iscritto"; + +/* alert title */ +"Remove subscriber?" = "Rimuovere l'iscritto?"; + /* No comment provided by engineer. */ "removed" = "rimosso"; +/* receive error chat item */ +"removed (%d attempts)" = "rimosso (%d tentativi)"; + /* rcv group event chat item */ "removed %@" = "ha rimosso %@"; +/* No comment provided by engineer. */ +"removed by operator" = "rimosso da un operatore"; + /* profile update event chat item */ "removed contact address" = "indirizzo di contatto rimosso"; @@ -4606,6 +5047,9 @@ swipe action */ /* No comment provided by engineer. */ "Run chat" = "Avvia chat"; +/* No comment provided by engineer. */ +"Safe web links" = "Link web sicuri"; + /* No comment provided by engineer. */ "Safely receive files" = "Ricevi i file in sicurezza"; @@ -4620,7 +5064,10 @@ chat item action */ "Save (and notify contacts)" = "Salva (e avvisa i contatti)"; /* alert button */ -"Save (and notify members)" = "Salva (e informa i membri)"; +"Save (and notify members)" = "Salva (e avvisa i membri)"; + +/* alert button */ +"Save (and notify subscribers)" = "Salva (e avvisa gli iscritti)"; /* alert title */ "Save admission settings?" = "Salvare le impostazioni di ammissione?"; @@ -4631,12 +5078,21 @@ chat item action */ /* No comment provided by engineer. */ "Save and notify group members" = "Salva e avvisa i membri del gruppo"; +/* No comment provided by engineer. */ +"Save and notify subscribers" = "Salva e avvisa gli iscritti"; + /* No comment provided by engineer. */ "Save and reconnect" = "Salva e riconnetti"; /* No comment provided by engineer. */ "Save and update group profile" = "Salva e aggiorna il profilo del gruppo"; +/* No comment provided by engineer. */ +"Save channel profile" = "Salva il profilo del canale"; + +/* alert title */ +"Save channel profile?" = "Salva il profilo del canale?"; + /* No comment provided by engineer. */ "Save group profile" = "Salva il profilo del gruppo"; @@ -4701,7 +5157,7 @@ chat item action */ "Scan code" = "Scansiona codice"; /* No comment provided by engineer. */ -"Scan QR code" = "Scansiona codice QR"; +"Scan QR code" = "Scansiona un codice QR"; /* No comment provided by engineer. */ "Scan QR code from desktop" = "Scansiona codice QR dal desktop"; @@ -4766,6 +5222,9 @@ chat item action */ /* chat item text */ "security code changed" = "codice di sicurezza modificato"; +/* No comment provided by engineer. */ +"Security: owners hold channel keys." = "Sicurezza: solo i proprietari hanno le chiavi del canale."; + /* chat item action */ "Select" = "Seleziona"; @@ -4844,12 +5303,18 @@ chat item action */ /* No comment provided by engineer. */ "Send request without message" = "Invia richiesta senza messaggio"; +/* No comment provided by engineer. */ +"Send the link via any messenger - it's secure. Ask to paste into SimpleX." = "Invia il link tramite qualsiasi messenger, è sicuro. Chiedi di incollarlo in SimpleX."; + /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Inviali dalla galleria o dalle tastiere personalizzate."; /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Invia fino a 100 ultimi messaggi ai nuovi membri."; +/* No comment provided by engineer. */ +"Send up to 100 last messages to new subscribers." = "Invia fino a 100 ultimi messaggi ai nuovi iscritti."; + /* No comment provided by engineer. */ "Send your private feedback to groups." = "Invia i tuoi commenti privati ai gruppi."; @@ -4859,6 +5324,9 @@ chat item action */ /* No comment provided by engineer. */ "Sender may have deleted the connection request." = "Il mittente potrebbe aver eliminato la richiesta di connessione."; +/* alert message */ +"Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later." = "L'invio di un'anteprima del link può rivelare il tuo indirizzo IP al sito. Puoi modificarlo nelle impostazioni di Privacy più tardi."; + /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "L'invio delle ricevute di consegna sarà attivo per tutti i contatti in tutti i profili di chat visibili."; @@ -4937,6 +5405,9 @@ chat item action */ /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "info coda server: %1$@\n\nultimo msg ricevuto: %2$@"; +/* relay test error */ +"Server requires authorization to connect to relay, check password." = "Il server richiede l'autorizzazione per connettersi al relay, controlla la password."; + /* server test error */ "Server requires authorization to create queues, check password." = "Il server richiede l'autorizzazione di creare code, controlla la password."; @@ -5021,6 +5492,12 @@ chat item action */ /* alert message */ "Settings were changed." = "Le impostazioni sono state cambiate."; +/* No comment provided by engineer. */ +"Setup notifications" = "Configura le notifiche"; + +/* No comment provided by engineer. */ +"Setup routers" = "Configura gli instradatori"; + /* No comment provided by engineer. */ "Shape profile images" = "Forma delle immagini del profilo"; @@ -5041,7 +5518,10 @@ chat item action */ "Share address publicly" = "Condividi indirizzo pubblicamente"; /* alert title */ -"Share address with contacts?" = "Condividere l'indirizzo con i contatti?"; +"Share address with SimpleX contacts?" = "Condividere l'indirizzo con i contatti di SimpleX?"; + +/* No comment provided by engineer. */ +"Share channel" = "Condividi canale"; /* No comment provided by engineer. */ "Share from other apps." = "Condividi da altre app."; @@ -5058,6 +5538,9 @@ chat item action */ /* No comment provided by engineer. */ "Share profile" = "Condividi il profilo"; +/* No comment provided by engineer. */ +"Share relay address" = "Condividi l'indirizzo del relay"; + /* No comment provided by engineer. */ "Share SimpleX address on social media." = "Condividi l'indirizzo SimpleX sui social media."; @@ -5068,7 +5551,10 @@ chat item action */ "Share to SimpleX" = "Condividi in SimpleX"; /* No comment provided by engineer. */ -"Share with contacts" = "Condividi con i contatti"; +"Share via chat" = "Condividi via chat"; + +/* No comment provided by engineer. */ +"Share with SimpleX contacts" = "Condividi con i contatti di SimpleX"; /* No comment provided by engineer. */ "Share your address" = "Condividi il tuo indirizzo"; @@ -5173,7 +5659,7 @@ chat item action */ "SimpleX protocols reviewed by Trail of Bits." = "Protocolli di SimpleX esaminati da Trail of Bits."; /* simplex link type */ -"SimpleX relay link" = "Link del relay SimpleX"; +"SimpleX relay address" = "Indirizzo del relay SimpleX"; /* No comment provided by engineer. */ "Simplified incognito mode" = "Modalità incognito semplificata"; @@ -5227,6 +5713,9 @@ report reason */ /* chat item text */ "standard end-to-end encryption" = "crittografia end-to-end standard"; +/* No comment provided by engineer. */ +"Star on GitHub" = "Dai una stella su GitHub"; + /* No comment provided by engineer. */ "Start chat" = "Avvia chat"; @@ -5291,7 +5780,49 @@ report reason */ "Submit" = "Invia"; /* No comment provided by engineer. */ -"Subscribed" = "Iscritto"; +"Subscribed" = "Iscritto/a"; + +/* No comment provided by engineer. */ +"Subscriber" = "Iscritto"; + +/* chat feature */ +"Subscriber reports" = "Segnalazioni degli iscritti"; + +/* alert message */ +"Subscriber will be removed from channel - this cannot be undone!" = "L'iscritto verrà rimosso dal canale, non è reversibile!"; + +/* No comment provided by engineer. */ +"Subscribers" = "Iscritti"; + +/* No comment provided by engineer. */ +"Subscribers can add message reactions." = "Gli iscritti al canale possono aggiungere reazioni ai messaggi."; + +/* No comment provided by engineer. */ +"Subscribers can chat with admins." = "Gli iscritti possono chattare con gli amministratori."; + +/* No comment provided by engineer. */ +"Subscribers can irreversibly delete sent messages. (24 hours)" = "Gli iscritti al canale possono eliminare irreversibilmente i messaggi inviati. (24 ore)"; + +/* No comment provided by engineer. */ +"Subscribers can report messsages to moderators." = "Gli iscritti possono segnalare messaggi ai moderatori."; + +/* No comment provided by engineer. */ +"Subscribers can send direct messages." = "Gli iscritti al canale possono inviare messaggi diretti."; + +/* No comment provided by engineer. */ +"Subscribers can send disappearing messages." = "Gli iscritti al canale possono inviare messaggi a tempo."; + +/* No comment provided by engineer. */ +"Subscribers can send files and media." = "Gli iscritti al canale possono inviare file e contenuti multimediali."; + +/* No comment provided by engineer. */ +"Subscribers can send SimpleX links." = "Gli iscritti al canale possono inviare link di Simplex."; + +/* No comment provided by engineer. */ +"Subscribers can send voice messages." = "Gli iscritti al canale possono inviare messaggi vocali."; + +/* No comment provided by engineer. */ +"Subscribers use relay link to connect to the channel.\nRelay address was used to set up this relay for the channel." = "Gli iscritti usano il link del relay per connettersi al canale.\nL'indirizzo del relay è stato usato per impostare questo relay per il canale."; /* No comment provided by engineer. */ "Subscription errors" = "Errori di iscrizione"; @@ -5320,6 +5851,9 @@ report reason */ /* No comment provided by engineer. */ "Take picture" = "Scatta foto"; +/* No comment provided by engineer. */ +"Talk to someone" = "Parla con qualcuno"; + /* No comment provided by engineer. */ "Tap button " = "Tocca il pulsante "; @@ -5333,7 +5867,7 @@ report reason */ "Tap Connect to use bot" = "Tocca Connetti per usare il bot"; /* No comment provided by engineer. */ -"Tap Create SimpleX address in the menu to create it later." = "Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi."; +"Tap Join channel" = "Tocca Iscriviti al canale"; /* No comment provided by engineer. */ "Tap Join group" = "Tocca Entra nel gruppo"; @@ -5350,6 +5884,9 @@ report reason */ /* No comment provided by engineer. */ "Tap to join incognito" = "Toccare per entrare in incognito"; +/* No comment provided by engineer. */ +"Tap to open" = "Tocca per aprire"; + /* No comment provided by engineer. */ "Tap to paste link" = "Tocca per incollare il link"; @@ -5380,12 +5917,16 @@ report reason */ /* file error alert title */ "Temporary file error" = "Errore del file temporaneo"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Test fallito al passo %@."; /* No comment provided by engineer. */ "Test notifications" = "Prova le notifiche"; +/* No comment provided by engineer. */ +"Test relay" = "Prova relay"; + /* No comment provided by engineer. */ "Test server" = "Prova server"; @@ -5413,6 +5954,9 @@ report reason */ /* No comment provided by engineer. */ "The app protects your privacy by using different operators in each conversation." = "L'app protegge la tua privacy usando diversi operatori in ogni conversazione."; +/* No comment provided by engineer. */ +"The app removed this message after %lld attempts to receive it." = "L'app ha rimosso questo messaggio dopo %lld tentativi di riceverlo."; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "L'app chiederà di confermare i download da server di file sconosciuti (eccetto .onion)."; @@ -5422,6 +5966,9 @@ report reason */ /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "Il codice che hai scansionato non è un codice QR di link SimpleX."; +/* conn error description */ +"The connection reached the limit of undelivered messages" = "La connessione ha raggiunto il limite di messaggi non consegnati"; + /* No comment provided by engineer. */ "The connection reached the limit of undelivered messages, your contact may be offline." = "La connessione ha raggiunto il limite di messaggi non consegnati, il contatto potrebbe essere offline."; @@ -5438,7 +5985,7 @@ report reason */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "La crittografia funziona e il nuovo accordo sulla crittografia non è richiesto. Potrebbero verificarsi errori di connessione!"; /* No comment provided by engineer. */ -"The future of messaging" = "La nuova generazione di messaggistica privata"; +"The first network where you own\nyour contacts and groups." = "La prima rete in cui possiedi\ni tuoi contatti e i tuoi gruppi."; /* No comment provided by engineer. */ "The hash of the previous message is different." = "L'hash del messaggio precedente è diverso."; @@ -5464,6 +6011,9 @@ report reason */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Il database vecchio non è stato rimosso durante la migrazione, può essere eliminato."; +/* No comment provided by engineer. */ +"The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it." = "La più antica libertà umana, parlare con un'altra persona senza essere osservati, si basa su un'infrastruttura che non può tradirla."; + /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Le stesse condizioni si applicheranno all'operatore **%@**."; @@ -5491,6 +6041,12 @@ report reason */ /* No comment provided by engineer. */ "Themes" = "Temi"; +/* No comment provided by engineer. */ +"Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible." = "Poi ci siamo trasferiti online e ogni piattaforma ha chiesto un pezzo di noi: il nome, il numero, gli amici. Abbiamo accettato che il prezzo da pagare per comunicare con gli altri fosse quello di far sapere a qualcuno con chi parliamo. Ogni generazione, sia di persone che di tecnologia, ha funzionato così: telefono, email, messenger, social media. Sembrava l'unico modo possibile."; + +/* No comment provided by engineer. */ +"There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected." = "C'è un'altra via. Una rete senza numeri di telefono. Senza nomi utente. Senza account. Senza identificatori utente di alcun tipo. Una rete che connette le persone e trasferisce messaggi crittografati senza sapere chi è connesso."; + /* No comment provided by engineer. */ "These conditions will also apply for: **%@**." = "Queste condizioni si applicheranno anche per: **%@**."; @@ -5533,6 +6089,12 @@ report reason */ /* No comment provided by engineer. */ "This group no longer exists." = "Questo gruppo non esiste più."; +/* alert message */ +"This is a chat relay address, it cannot be used to connect." = "Questo è un indirizzo di relay di chat, non può essere usato per connettersi."; + +/* new chat action */ +"This is your link for channel %@!" = "Questo è il tuo link per il canale %@!"; + /* No comment provided by engineer. */ "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Questo link richiede una versione più recente dell'app. Aggiornala o chiedi al tuo contatto di inviare un link compatibile."; @@ -5566,6 +6128,9 @@ report reason */ /* No comment provided by engineer. */ "To make a new connection" = "Per creare una nuova connessione"; +/* No comment provided by engineer. */ +"To make SimpleX Network last." = "Per la sostenibilità della rete di SimpleX."; + /* No comment provided by engineer. */ "To protect against your link being replaced, you can compare contact security codes." = "Per proteggerti dalla sostituzione del tuo link, puoi confrontare i codici di sicurezza del contatto."; @@ -5614,9 +6179,6 @@ report reason */ /* 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 chat list:" = "Cambia l'elenco delle chat:"; - /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Attiva/disattiva l'incognito quando ti colleghi."; @@ -5626,6 +6188,9 @@ report reason */ /* No comment provided by engineer. */ "Toolbar opacity" = "Opacità barra degli strumenti"; +/* No comment provided by engineer. */ +"Top bar" = "Barra superiore"; + /* No comment provided by engineer. */ "Total" = "Totale"; @@ -5665,6 +6230,9 @@ report reason */ /* No comment provided by engineer. */ "Unblock member?" = "Sbloccare il membro?"; +/* No comment provided by engineer. */ +"Unblock subscriber for all?" = "Sbloccare l'iscritto per tutti?"; + /* rcv group event chat item */ "unblocked %@" = "ha sbloccato %@"; @@ -5737,12 +6305,15 @@ report reason */ /* swipe action */ "Unread" = "Non letto"; -/* No comment provided by engineer. */ +/* conn error description */ "Unsupported connection link" = "Link di connessione non supportato"; /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Vengono inviati ai nuovi membri fino a 100 ultimi messaggi."; +/* No comment provided by engineer. */ +"Up to 100 last messages are sent to new subscribers." = "Vengono inviati ai nuovi iscritti fino a 100 ultimi messaggi."; + /* No comment provided by engineer. */ "Update" = "Aggiorna"; @@ -5755,6 +6326,9 @@ report reason */ /* No comment provided by engineer. */ "Update settings?" = "Aggiornare le impostazioni?"; +/* rcv group event chat item */ +"updated channel profile" = "profilo del canale aggiornato"; + /* No comment provided by engineer. */ "Updated conditions" = "Condizioni aggiornate"; @@ -5812,9 +6386,6 @@ report reason */ /* No comment provided by engineer. */ "Use %@" = "Usa %@"; -/* No comment provided by engineer. */ -"Use chat" = "Usa la chat"; - /* new chat action */ "Use current profile" = "Usa il profilo attuale"; @@ -5824,6 +6395,9 @@ report reason */ /* No comment provided by engineer. */ "Use for messages" = "Usa per i messaggi"; +/* No comment provided by engineer. */ +"Use for new channels" = "Usa per canali nuovi"; + /* No comment provided by engineer. */ "Use for new connections" = "Usa per connessioni nuove"; @@ -5848,6 +6422,9 @@ report reason */ /* No comment provided by engineer. */ "Use private routing with unknown servers." = "Usa l'instradamento privato con server sconosciuti."; +/* No comment provided by engineer. */ +"Use relay" = "Usa relay"; + /* No comment provided by engineer. */ "Use server" = "Usa il server"; @@ -5872,6 +6449,9 @@ report reason */ /* No comment provided by engineer. */ "Use the app with one hand." = "Usa l'app con una mano sola."; +/* No comment provided by engineer. */ +"Use this address in your social media profile, website, or email signature." = "Usa questo indirizzo nel tuo profilo di social media, sito web o firma email."; + /* No comment provided by engineer. */ "Use web port" = "Usa porta web"; @@ -5890,6 +6470,9 @@ report reason */ /* No comment provided by engineer. */ "v%@ (%@)" = "v%@ (%@)"; +/* relay test step */ +"Verify" = "Verifica"; + /* No comment provided by engineer. */ "Verify code with desktop" = "Verifica il codice con il desktop"; @@ -5911,6 +6494,9 @@ report reason */ /* No comment provided by engineer. */ "Verify security code" = "Verifica codice di sicurezza"; +/* relay hostname */ +"via %@" = "via %@"; + /* No comment provided by engineer. */ "Via browser" = "Via browser"; @@ -5980,9 +6566,18 @@ report reason */ /* No comment provided by engineer. */ "Voice messages prohibited!" = "Messaggi vocali vietati!"; +/* alert action */ +"Wait" = "Attendi"; + +/* relay test step */ +"Wait response" = "Attendi risposta"; + /* No comment provided by engineer. */ "waiting for answer…" = "in attesa di risposta…"; +/* No comment provided by engineer. */ +"Waiting for channel owner to add relays." = "In attesa che il proprietario del canale aggiunga dei relay."; + /* No comment provided by engineer. */ "waiting for confirmation…" = "in attesa di conferma…"; @@ -6013,6 +6608,9 @@ report reason */ /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Attenzione: potresti perdere alcuni dati!"; +/* No comment provided by engineer. */ +"We made connecting simpler for new users." = "Abbiamo semplificato la connessione per i nuovi utenti."; + /* No comment provided by engineer. */ "WebRTC ICE servers" = "Server WebRTC ICE"; @@ -6049,6 +6647,9 @@ report reason */ /* 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." = "Quando condividi un profilo in incognito con qualcuno, questo profilo verrà utilizzato per i gruppi a cui ti invitano."; +/* No comment provided by engineer. */ +"Why SimpleX is built." = "Perché costruiamo SimpleX."; + /* No comment provided by engineer. */ "WiFi" = "WiFi"; @@ -6148,6 +6749,9 @@ report reason */ /* No comment provided by engineer. */ "you are observer" = "sei un osservatore"; +/* No comment provided by engineer. */ +"you are subscriber" = "sei iscritto/a"; + /* snd group event chat item */ "you blocked %@" = "hai bloccato %@"; @@ -6190,6 +6794,9 @@ report reason */ /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Puoi impostare l'anteprima della notifica nella schermata di blocco tramite le impostazioni."; +/* No comment provided by engineer. */ +"You can share a link or a QR code - anybody will be able to join the channel." = "Puoi condividere un link o un codice QR, chiunque sarà in grado di iscriversi al canale."; + /* 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." = "Puoi condividere un link o un codice QR: chiunque potrà unirsi al gruppo. Non perderai i membri del gruppo se in seguito lo elimini."; @@ -6230,10 +6837,13 @@ report reason */ "you changed role of %@ to %@" = "hai cambiato il ruolo di %1$@ in %2$@"; /* No comment provided by engineer. */ -"You could not be verified; please try again." = "Non è stato possibile verificarti, riprova."; +"You commit to:\n- Only legal content in public groups\n- Respect other users - no spam" = "Tu ti impegni a:\n- Pubblicare solo contenuto legale nei gruppi pubblici\n- Rispettare gli altri utenti. Niente spam"; /* No comment provided by engineer. */ -"You decide who can connect." = "Sei tu a decidere chi può connettersi."; +"You connected to the channel via this relay link." = "Ti sei connesso/a al canale attraverso questo link del relay."; + +/* No comment provided by engineer. */ +"You could not be verified; please try again." = "Non è stato possibile verificarti, riprova."; /* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Hai già richiesto la connessione!\nRipetere la richiesta di connessione?"; @@ -6289,6 +6899,9 @@ report reason */ /* snd group event chat item */ "you unblocked %@" = "hai sbloccato %@"; +/* No comment provided by engineer. */ +"You were born without an account" = "Sei nato senza un account"; + /* No comment provided by engineer. */ "You will be able to send messages **only after your request is accepted**." = "Potrai inviare messaggi **solo dopo che la tua richiesta verrà accettata**."; @@ -6310,6 +6923,9 @@ report reason */ /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Continuerai a ricevere chiamate e notifiche da profili silenziati quando sono attivi."; +/* No comment provided by engineer. */ +"You will stop receiving messages from this channel. Chat history will be preserved." = "Smetterai di ricevere messaggi da questo canale. La cronologia della chat sarà preservata."; + /* No comment provided by engineer. */ "You will stop receiving messages from this chat. Chat history will be preserved." = "Non riceverai più messaggi da questa chat. La cronologia della chat verrà conservata."; @@ -6334,6 +6950,9 @@ report reason */ /* No comment provided by engineer. */ "Your calls" = "Le tue chiamate"; +/* No comment provided by engineer. */ +"Your channel" = "Il tuo canale"; + /* No comment provided by engineer. */ "Your chat database" = "Il tuo database della chat"; @@ -6364,6 +6983,9 @@ report reason */ /* No comment provided by engineer. */ "Your contacts will remain connected." = "I tuoi contatti resteranno connessi."; +/* No comment provided by engineer. */ +"Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public." = "Le tue conversazioni appartengono a te, come è sempre stato prima dell'avvento di internet. La rete non è un luogo che visiti. È un luogo che crei e possiedi. E nessuno può portartelo via, che tu lo renda privato o pubblico."; + /* No comment provided by engineer. */ "Your credentials may be sent unencrypted." = "Le credenziali potrebbero essere inviate in chiaro."; @@ -6379,6 +7001,9 @@ report reason */ /* No comment provided by engineer. */ "Your ICE servers" = "I tuoi server ICE"; +/* No comment provided by engineer. */ +"Your network" = "La tua rete"; + /* No comment provided by engineer. */ "Your preferences" = "Le tue preferenze"; @@ -6388,6 +7013,9 @@ report reason */ /* No comment provided by engineer. */ "Your profile" = "Il tuo profilo"; +/* No comment provided by engineer. */ +"Your profile **%@** will be shared with channel relays and subscribers.\nRelays can access channel messages." = "Il tuo profilo **%@** verrà condiviso con i relay e gli iscritti.\nI relay hanno accesso ai messaggi del canale."; + /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Verrà condiviso il tuo profilo **%@**."; @@ -6400,9 +7028,18 @@ report reason */ /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Il tuo profilo è stato cambiato. Se lo salvi, il profilo aggiornato verrà inviato a tutti i tuoi contatti."; +/* No comment provided by engineer. */ +"Your public address" = "Il tuo indirizzo pubblico"; + /* No comment provided by engineer. */ "Your random profile" = "Il tuo profilo casuale"; +/* No comment provided by engineer. */ +"Your relay address" = "L'indirizzo del tuo relay"; + +/* No comment provided by engineer. */ +"Your relay name" = "Il nome del tuo relay"; + /* No comment provided by engineer. */ "Your server address" = "L'indirizzo del tuo server"; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 480eb39d36..b14777244f 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -25,15 +25,9 @@ /* No comment provided by engineer. */ "(this device v%@)" = "(このデバイス v%@)"; -/* 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. */ "**Create 1-time link**: to create and share a new invitation link." = "**コンタクトの追加**: 新しい招待リンクを作成するか、受け取ったリンクから接続します。"; @@ -377,9 +371,6 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "アクティブな接続"; -/* 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 friends" = "友達を追加"; @@ -566,9 +557,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "通話に応答"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。"; - /* No comment provided by engineer. */ "App build: %@" = "アプリのビルド: %@"; @@ -890,7 +878,8 @@ set passcode view */ /* No comment provided by engineer. */ "Confirm password" = "パスワードを確認"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "接続"; /* No comment provided by engineer. */ @@ -968,7 +957,7 @@ set passcode view */ /* alert title */ "Connection error" = "接続エラー"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "接続エラー (AUTH)"; /* chat list item title (it should not be shown */ @@ -1019,15 +1008,15 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "続ける"; +/* No comment provided by engineer. */ +"Contribute" = "貢献する"; + /* No comment provided by engineer. */ "Copy" = "コピー"; /* No comment provided by engineer. */ "Core version: v%@" = "コアのバージョン: v%@"; -/* No comment provided by engineer. */ -"Create" = "作成"; - /* server test step */ "Create file" = "ファイルを作成"; @@ -1142,9 +1131,6 @@ set passcode view */ /* No comment provided by engineer. */ "Debug delivery" = "配信のデバッグ"; -/* No comment provided by engineer. */ -"Decentralized" = "分散型"; - /* message decrypt error item */ "Decryption error" = "復号化エラー"; @@ -1385,7 +1371,7 @@ alert button */ /* No comment provided by engineer. */ "Edit group profile" = "グループのプロフィールを編集"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "有効"; /* No comment provided by engineer. */ @@ -1403,9 +1389,6 @@ alert button */ /* No comment provided by engineer. */ "Enable lock" = "ロックモード"; -/* No comment provided by engineer. */ -"Enable notifications" = "通知を有効化"; - /* No comment provided by engineer. */ "Enable periodic notifications?" = "定期的な通知を有効にしますか?"; @@ -1517,7 +1500,7 @@ alert button */ /* No comment provided by engineer. */ "error" = "エラー"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "エラー"; /* No comment provided by engineer. */ @@ -1720,7 +1703,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "チャットを素早く検索"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "サーバアドレスの証明証IDが正しくないかもしれません"; /* No comment provided by engineer. */ @@ -1786,7 +1770,7 @@ snd error text */ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "グループ招待が無効となり、送信元によって取り消されました。"; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "グループのリンク"; /* No comment provided by engineer. */ @@ -1888,9 +1872,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "即座に"; -/* No comment provided by engineer. */ -"Immune to spam" = "スパムや悪質送信を防止"; - /* No comment provided by engineer. */ "Import" = "読み込む"; @@ -1955,7 +1936,7 @@ snd error text */ "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)"; +"Install SimpleX Chat for terminal" = "インストール ターミナル用SimpleX Chat"; /* No comment provided by engineer. */ "Instant" = "即時"; @@ -1972,7 +1953,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "無効なチャットデータ"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "無効な接続リンク"; /* invalid chat item */ @@ -2215,9 +2196,6 @@ snd error text */ /* No comment provided by engineer. */ "Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "メッセージ、ファイル、通話は、前方秘匿性、否認可能性および侵入復元性を備えた**耐量子E2E暗号化**によって保護されます。"; -/* No comment provided by engineer. */ -"Migrate from another device" = "別の端末から移行"; - /* No comment provided by engineer. */ "Migrating database archive…" = "データベースのアーカイブを移行しています…"; @@ -2362,9 +2340,6 @@ snd error text */ /* copied message info in history */ "no text" = "テキストなし"; -/* No comment provided by engineer. */ -"No user identifiers." = "世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。"; - /* No comment provided by engineer. */ "Notifications" = "通知"; @@ -2457,7 +2432,8 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "音声メッセージを送れるのはあなたの連絡相手だけです。"; -/* alert action */ +/* alert action +alert button */ "Open" = "開く"; /* new chat action */ @@ -2559,9 +2535,6 @@ new chat action */ /* No comment provided by engineer. */ "Privacy & security" = "プライバシーとセキュリティ"; -/* No comment provided by engineer. */ -"Privacy redefined" = "プライバシーの基準を新境地に"; - /* No comment provided by engineer. */ "Private filenames" = "プライベートなファイル名"; @@ -2577,9 +2550,6 @@ new chat action */ /* No comment provided by engineer. */ "Profile password" = "プロフィールのパスワード"; -/* alert message */ -"Profile update will be sent to your contacts." = "連絡先にプロフィール更新のお知らせが届きます。"; - /* No comment provided by engineer. */ "Prohibit audio/video calls." = "音声/ビデオ通話を禁止する 。"; @@ -2632,13 +2602,10 @@ new chat action */ "Read more" = "続きを読む"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)をご覧ください。"; +"Read more in our GitHub repository." = "詳しくはGitHubリポジトリをご覧ください。"; /* 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)をご覧ください。"; +"Read more in User Guide." = "詳しくはユーザーガイドをご覧ください。"; /* No comment provided by engineer. */ "received answer…" = "回答を受け取りました…"; @@ -2977,15 +2944,9 @@ chat item action */ /* No comment provided by engineer. */ "Share address" = "アドレスを共有する"; -/* alert title */ -"Share address with contacts?" = "アドレスを連絡先と共有しますか?"; - /* No comment provided by engineer. */ "Share link" = "リンクを送る"; -/* No comment provided by engineer. */ -"Share with contacts" = "連絡先と共有する"; - /* No comment provided by engineer. */ "Show calls in phone history" = "通話履歴を表示"; @@ -3055,6 +3016,9 @@ chat item action */ /* notification title */ "Somebody" = "誰か"; +/* No comment provided by engineer. */ +"Star on GitHub" = "GitHub でスターを付ける"; + /* No comment provided by engineer. */ "Start chat" = "チャットを開始する"; @@ -3133,7 +3097,8 @@ chat item action */ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "テストはステップ %@ で失敗しました。"; /* No comment provided by engineer. */ @@ -3172,9 +3137,6 @@ chat item action */ /* 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 future of messaging" = "次世代のプライバシー・メッセンジャー"; - /* No comment provided by engineer. */ "The hash of the previous message is different." = "以前のメッセージとハッシュ値が異なります。"; @@ -3337,9 +3299,6 @@ chat item action */ /* No comment provided by engineer. */ "Use .onion hosts" = ".onionホストを使う"; -/* No comment provided by engineer. */ -"Use chat" = "チャット"; - /* new chat action */ "Use current profile" = "現在のプロファイルを使用する"; @@ -3547,9 +3506,6 @@ chat item action */ /* No comment provided by engineer. */ "You could not be verified; please try again." = "確認できませんでした。 もう一度お試しください。"; -/* No comment provided by engineer. */ -"You decide who can connect." = "あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 29f8bb5b3f..e12e255488 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -25,15 +25,9 @@ /* No comment provided by engineer. */ "(this device v%@)" = "(dit apparaat v%@)"; -/* No comment provided by engineer. */ -"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Bijdragen](https://github.com/simplex-chat/simplex-chat#contribute)"; - /* No comment provided by engineer. */ "[Send us email](mailto:chat@simplex.chat)" = "[Stuur ons een e-mail](mailto:chat@simplex.chat)"; -/* No comment provided by engineer. */ -"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)"; - /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Contact toevoegen**: om een nieuwe uitnodigingslink aan te maken, of verbinding te maken via een link die u heeft ontvangen."; @@ -395,9 +389,6 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "Actieve verbindingen"; -/* 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." = "Voeg een adres toe aan uw profiel, zodat uw contacten het met andere mensen kunnen delen. Profiel update wordt naar uw contacten verzonden."; - /* No comment provided by engineer. */ "Add friends" = "Vrienden toevoegen"; @@ -635,9 +626,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Beantwoord oproep"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "Iedereen kan servers hosten."; - /* No comment provided by engineer. */ "App build: %@" = "App build: %@"; @@ -867,7 +855,7 @@ marked deleted chat item preview text */ /* 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. */ +/* chat link info line */ "Business address" = "Zakelijk adres"; /* No comment provided by engineer. */ @@ -879,9 +867,6 @@ marked deleted chat item preview text */ /* 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 chatprofiel (standaard) of [via verbinding](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; -/* No comment provided by engineer. */ -"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "Door SimpleX Chat te gebruiken, gaat u ermee akkoord:\n- alleen legale content te versturen in openbare groepen.\n- andere gebruikers te respecteren – geen spam."; - /* No comment provided by engineer. */ "call" = "bellen"; @@ -1062,7 +1047,8 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "De chat wordt voor je verwijderd - dit kan niet ongedaan worden gemaakt!"; -/* chat toolbar */ +/* chat feature +chat toolbar */ "Chat with admins" = "Chat met beheerders"; /* No comment provided by engineer. */ @@ -1173,9 +1159,6 @@ set passcode view */ /* No comment provided by engineer. */ "Configure ICE servers" = "ICE servers configureren"; -/* No comment provided by engineer. */ -"Configure server operators" = "Serveroperators configureren"; - /* No comment provided by engineer. */ "Confirm" = "Bevestigen"; @@ -1209,7 +1192,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Bevestigd"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Verbind"; /* No comment provided by engineer. */ @@ -1305,7 +1289,7 @@ set passcode view */ /* alert title */ "Connection error" = "Verbindingsfout"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "Verbindingsfout (AUTH)"; /* chat list item title (it should not be shown */ @@ -1401,6 +1385,9 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "Doorgaan"; +/* No comment provided by engineer. */ +"Contribute" = "Bijdragen"; + /* No comment provided by engineer. */ "Conversation deleted!" = "Gesprek verwijderd!"; @@ -1416,12 +1403,9 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Hoek"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Juiste naam voor %@?"; -/* No comment provided by engineer. */ -"Create" = "Maak"; - /* No comment provided by engineer. */ "Create 1-time link" = "Eenmalige link maken"; @@ -1572,9 +1556,6 @@ set passcode view */ /* No comment provided by engineer. */ "Debug delivery" = "Foutopsporing bezorging"; -/* No comment provided by engineer. */ -"Decentralized" = "Gedecentraliseerd"; - /* message decrypt error item */ "Decryption error" = "Decodering fout"; @@ -1963,7 +1944,7 @@ chat item action */ /* No comment provided by engineer. */ "Edit group profile" = "Groep profiel bewerken"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "Inschakelen"; /* No comment provided by engineer. */ @@ -1990,9 +1971,6 @@ chat item action */ /* No comment provided by engineer. */ "Enable lock" = "Vergrendeling inschakelen"; -/* No comment provided by engineer. */ -"Enable notifications" = "Meldingen aanzetten"; - /* No comment provided by engineer. */ "Enable periodic notifications?" = "Periodieke meldingen inschakelen?"; @@ -2134,7 +2112,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "fout"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "Fout"; /* No comment provided by engineer. */ @@ -2499,7 +2477,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Vind chats sneller"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Mogelijk is de certificaat vingerafdruk in het server adres onjuist"; /* No comment provided by engineer. */ @@ -2523,7 +2502,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Voor alle moderators"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "Voor chatprofiel %@:"; /* No comment provided by engineer. */ @@ -2652,7 +2632,7 @@ snd error text */ /* No comment provided by engineer. */ "group is deleted" = "groep is verwijderd"; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "Groep link"; /* No comment provided by engineer. */ @@ -2775,9 +2755,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "Onmiddellijk"; -/* No comment provided by engineer. */ -"Immune to spam" = "Immuun voor spam en misbruik"; - /* No comment provided by engineer. */ "Import" = "Importeren"; @@ -2878,7 +2855,7 @@ snd error text */ "Initial role" = "Initiële rol"; /* No comment provided by engineer. */ -"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Installeer [SimpleX Chat voor terminal](https://github.com/simplex-chat/simplex-chat)"; +"Install SimpleX Chat for terminal" = "Installeer SimpleX Chat voor terminal"; /* No comment provided by engineer. */ "Instant" = "Direct"; @@ -2913,7 +2890,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "ongeldige gesprek gegevens"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "Ongeldige verbinding link"; /* invalid chat item */ @@ -2928,7 +2905,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Ongeldige migratie bevestiging"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Ongeldige naam!"; /* No comment provided by engineer. */ @@ -3327,9 +3304,6 @@ snd error text */ /* No comment provided by engineer. */ "Migrate device" = "Apparaat migreren"; -/* No comment provided by engineer. */ -"Migrate from another device" = "Migreer vanaf een ander apparaat"; - /* No comment provided by engineer. */ "Migrate here" = "Migreer hierheen"; @@ -3600,9 +3574,6 @@ snd error text */ /* No comment provided by engineer. */ "No unread chats" = "Geen ongelezen chats"; -/* No comment provided by engineer. */ -"No user identifiers." = "Geen gebruikers-ID's."; - /* No comment provided by engineer. */ "Not compatible!" = "Niet compatibel!"; @@ -3659,7 +3630,7 @@ alert button new chat action */ "Ok" = "OK"; -/* No comment provided by engineer. */ +/* alert button */ "OK" = "OK"; /* No comment provided by engineer. */ @@ -3734,7 +3705,8 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Alleen uw contact kan spraak berichten verzenden."; -/* alert action */ +/* alert action +alert button */ "Open" = "Open"; /* No comment provided by engineer. */ @@ -3965,12 +3937,6 @@ new chat action */ /* No comment provided by engineer. */ "Privacy policy and conditions of use." = "Privacybeleid en gebruiksvoorwaarden."; -/* No comment provided by engineer. */ -"Privacy redefined" = "Privacy opnieuw gedefinieerd"; - -/* No comment provided by engineer. */ -"Private chats, groups and your contacts are not accessible to server operators." = "Privéchats, groepen en uw contacten zijn niet toegankelijk voor serverbeheerders."; - /* No comment provided by engineer. */ "Private filenames" = "Privé bestandsnamen"; @@ -4007,9 +3973,6 @@ new chat action */ /* No comment provided by engineer. */ "Profile theme" = "Profiel thema"; -/* alert message */ -"Profile update will be sent to your contacts." = "Profiel update wordt naar uw contacten verzonden."; - /* No comment provided by engineer. */ "Prohibit audio/video calls." = "Audio/video gesprekken verbieden."; @@ -4095,16 +4058,10 @@ new chat action */ "Read more" = "Lees meer"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in our GitHub repository." = "Lees meer in onze GitHub-repository."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/app-settings.html#uw-simplex-contactadres)."; - -/* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Lees meer in de [Gebruikershandleiding](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)." = "Lees meer in onze [GitHub-repository](https://github.com/simplex-chat/simplex-chat#readme)."; +"Read more in User Guide." = "Lees meer in de Gebruikershandleiding."; /* No comment provided by engineer. */ "Receipts are disabled" = "Bevestigingen zijn uitgeschakeld"; @@ -4794,9 +4751,6 @@ chat item action */ /* No comment provided by engineer. */ "Share address publicly" = "Adres openbaar delen"; -/* alert title */ -"Share address with contacts?" = "Adres delen met contacten?"; - /* No comment provided by engineer. */ "Share from other apps." = "Delen vanuit andere apps."; @@ -4815,9 +4769,6 @@ chat item action */ /* No comment provided by engineer. */ "Share to SimpleX" = "Delen op SimpleX"; -/* No comment provided by engineer. */ -"Share with contacts" = "Delen met contacten"; - /* No comment provided by engineer. */ "Short link" = "Korte link"; @@ -4963,6 +4914,9 @@ report reason */ /* chat item text */ "standard end-to-end encryption" = "standaard end-to-end encryptie"; +/* No comment provided by engineer. */ +"Star on GitHub" = "Star on GitHub"; + /* No comment provided by engineer. */ "Start chat" = "Begin gesprek"; @@ -5059,9 +5013,6 @@ report reason */ /* No comment provided by engineer. */ "Tap button " = "Tik op de knop "; -/* No comment provided by engineer. */ -"Tap Create SimpleX address in the menu to create it later." = "Tik op SimpleX-adres maken in het menu om het later te maken."; - /* No comment provided by engineer. */ "Tap to activate profile." = "Tik hier om profiel te activeren."; @@ -5101,7 +5052,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "Tijdelijke bestandsfout"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Test mislukt bij stap %@."; /* No comment provided by engineer. */ @@ -5155,9 +5107,6 @@ report reason */ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "De versleuteling werkt en de nieuwe versleutelingsovereenkomst is niet vereist. Dit kan leiden tot verbindingsfouten!"; -/* No comment provided by engineer. */ -"The future of messaging" = "De volgende generatie privéberichten"; - /* No comment provided by engineer. */ "The hash of the previous message is different." = "De hash van het vorige bericht is anders."; @@ -5317,9 +5266,6 @@ report reason */ /* 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 contact te verifiëren."; -/* No comment provided by engineer. */ -"Toggle chat list:" = "Chatlijst wisselen:"; - /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Schakel incognito in tijdens het verbinden."; @@ -5437,7 +5383,7 @@ report reason */ /* swipe action */ "Unread" = "Ongelezen"; -/* No comment provided by engineer. */ +/* conn error description */ "Unsupported connection link" = "Niet-ondersteunde verbindingslink"; /* No comment provided by engineer. */ @@ -5494,9 +5440,6 @@ report reason */ /* No comment provided by engineer. */ "Use %@" = "Gebruik %@"; -/* No comment provided by engineer. */ -"Use chat" = "Gebruik chat"; - /* new chat action */ "Use current profile" = "Gebruik het huidige profiel"; @@ -5899,9 +5842,6 @@ report reason */ /* No comment provided by engineer. */ "You could not be verified; please try again." = "U kon niet worden geverifieerd; probeer het opnieuw."; -/* No comment provided by engineer. */ -"You decide who can connect." = "Jij bepaalt wie er verbinding mag maken."; - /* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Je hebt al verbinding aangevraagd!\nVerbindingsverzoek herhalen?"; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index ed1f8850d8..e2e46590c9 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -25,15 +25,9 @@ /* No comment provided by engineer. */ "(this device v%@)" = "(to urządzenie v%@)"; -/* No comment provided by engineer. */ -"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Przyczyń się](https://github.com/simplex-chat/simplex-chat#contribute)"; - /* No comment provided by engineer. */ "[Send us email](mailto:chat@simplex.chat)" = "[Wyślij do nas email](mailto:chat@simplex.chat)"; -/* No comment provided by engineer. */ -"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Daj gwiazdkę na GitHub](https://github.com/simplex-chat/simplex-chat)"; - /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Dodaj kontakt**: aby utworzyć nowy link z zaproszeniem lub połączyć się za pomocą otrzymanego linku."; @@ -398,9 +392,6 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "Aktywne połączenia"; -/* 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." = "Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów."; - /* No comment provided by engineer. */ "Add friends" = "Dodaj znajomych"; @@ -650,9 +641,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Odbierz połączenie"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "Każdy może hostować serwery."; - /* No comment provided by engineer. */ "App build: %@" = "Kompilacja aplikacji: %@"; @@ -794,6 +782,12 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "Zły identyfikator wiadomości"; +/* No comment provided by engineer. */ +"Be free in your network." = "Ciesz się swobodą w swojej sieci."; + +/* No comment provided by engineer. */ +"Because we destroyed the power to know who you are. So that your power can never be taken." = "Ponieważ zniszczyliśmy moc pozwalającą poznać, kim jesteś. Więc twoja moc nigdy nie będzie Ci odebrana."; + /* No comment provided by engineer. */ "Better calls" = "Lepsze połączenia"; @@ -897,7 +891,7 @@ marked deleted chat item preview text */ /* 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. */ +/* chat link info line */ "Business address" = "Adres firmowy"; /* No comment provided by engineer. */ @@ -912,9 +906,6 @@ marked deleted chat item preview text */ /* 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)."; -/* No comment provided by engineer. */ -"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "Korzystając z SimpleX Chat, zgadzasz się:\n- wysyłać tylko legalne treści w grupach publicznych.\n- szanować innych użytkowników – nie spamować."; - /* No comment provided by engineer. */ "call" = "zadzwoń"; @@ -1098,7 +1089,8 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "Czat zostanie usunięty dla Ciebie – tej operacji nie można cofnąć!"; -/* chat toolbar */ +/* chat feature +chat toolbar */ "Chat with admins" = "Czatuj z administratorami"; /* No comment provided by engineer. */ @@ -1212,9 +1204,6 @@ set passcode view */ /* No comment provided by engineer. */ "Configure ICE servers" = "Skonfiguruj serwery ICE"; -/* No comment provided by engineer. */ -"Configure server operators" = "Skonfiguruj operatorów serwerów"; - /* No comment provided by engineer. */ "Confirm" = "Potwierdź"; @@ -1248,7 +1237,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Potwierdzony"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Połącz"; /* No comment provided by engineer. */ @@ -1347,12 +1337,15 @@ set passcode view */ /* alert title */ "Connection error" = "Błąd połączenia"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "Błąd połączenia (UWIERZYTELNIANIE)"; /* chat list item title (it should not be shown */ "connection established" = "połączenie ustanowione"; +/* No comment provided by engineer. */ +"Connection failed" = "Połączenie nie powiodło się"; + /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "Połączenie zostało zablokowane przez operatora serwera:\n%@"; @@ -1449,6 +1442,9 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "Kontynuuj"; +/* No comment provided by engineer. */ +"Contribute" = "Przyczyń się"; + /* No comment provided by engineer. */ "Conversation deleted!" = "Rozmowa usunięta!"; @@ -1464,12 +1460,9 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Róg"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Poprawić imię na %@?"; -/* No comment provided by engineer. */ -"Create" = "Utwórz"; - /* No comment provided by engineer. */ "Create 1-time link" = "Utwórz jednorazowy link"; @@ -1623,9 +1616,6 @@ set passcode view */ /* No comment provided by engineer. */ "Debug delivery" = "Dostarczenie debugowania"; -/* No comment provided by engineer. */ -"Decentralized" = "Zdecentralizowane"; - /* message decrypt error item */ "Decryption error" = "Błąd odszyfrowania"; @@ -2029,7 +2019,7 @@ chat item action */ /* No comment provided by engineer. */ "Empty message!" = "Pusta wiadomość!"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "Włącz"; /* No comment provided by engineer. */ @@ -2059,9 +2049,6 @@ chat item action */ /* No comment provided by engineer. */ "Enable lock" = "Włącz blokadę"; -/* No comment provided by engineer. */ -"Enable notifications" = "Włącz powiadomienia"; - /* No comment provided by engineer. */ "Enable periodic notifications?" = "Włączyć okresowe powiadomienia?"; @@ -2203,7 +2190,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "błąd"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "Błąd"; /* No comment provided by engineer. */ @@ -2338,9 +2325,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Błąd otwierania czatu"; -/* No comment provided by engineer. */ -"Error opening group" = "Błąd otwierania grupy"; - /* alert title */ "Error receiving file" = "Błąd odbioru pliku"; @@ -2454,7 +2438,8 @@ file error text snd error text */ "Error: %@" = "Błąd: %@"; -/* server test error */ +/* relay test error +server test error */ "Error: %@." = "Błąd: %@."; /* No comment provided by engineer. */ @@ -2502,6 +2487,9 @@ snd error text */ /* No comment provided by engineer. */ "Exporting database archive…" = "Eksportowanie archiwum bazy danych…"; +/* No comment provided by engineer. */ +"failed" = "nieudane"; + /* No comment provided by engineer. */ "Failed to remove passphrase" = "Nie udało się usunąć hasła"; @@ -2604,7 +2592,8 @@ snd error text */ /* No comment provided by engineer. */ "Fingerprint in server address does not match certificate: %@." = "Odcisk palca w adresie serwera nie zgadza się z certyfikatem: %@."; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy."; /* No comment provided by engineer. */ @@ -2628,7 +2617,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Dla wszystkich moderatorów"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "Dla profilu czatu %@:"; /* No comment provided by engineer. */ @@ -2760,7 +2750,7 @@ snd error text */ /* No comment provided by engineer. */ "group is deleted" = "grupa została usunięta"; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "Link do grupy"; /* No comment provided by engineer. */ @@ -2871,6 +2861,9 @@ snd error text */ /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "Jeśli wpiszesz swój pin samodestrukcji podczas otwierania aplikacji:"; +/* down migration warning */ +"If you joined or created channels, they will stop working permanently." = "Jeśli dołączyłeś do kanałów lub je utworzyłeś, przestaną one działać na stałe."; + /* 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)." = "Jeśli potrzebujesz użyć czatu teraz, dotknij **Zrób to później** poniżej (zostanie Ci zaproponowana migracja bazy danych po ponownym uruchomieniu aplikacji)."; @@ -2889,9 +2882,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "Natychmiast"; -/* No comment provided by engineer. */ -"Immune to spam" = "Odporność na spam i nadużycia"; - /* No comment provided by engineer. */ "Import" = "Importuj"; @@ -2992,7 +2982,7 @@ snd error text */ "Initial role" = "Rola początkowa"; /* No comment provided by engineer. */ -"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Zainstaluj [SimpleX Chat na terminal](https://github.com/simplex-chat/simplex-chat)"; +"Install SimpleX Chat for terminal" = "Zainstaluj SimpleX Chat na terminal"; /* No comment provided by engineer. */ "Instant" = "Natychmiastowo"; @@ -3027,7 +3017,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "nieprawidłowe dane czatu"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "Nieprawidłowy link połączenia"; /* invalid chat item */ @@ -3042,7 +3032,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Nieprawidłowe potwierdzenie migracji"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Nieprawidłowa nazwa!"; /* No comment provided by engineer. */ @@ -3471,9 +3461,6 @@ snd error text */ /* No comment provided by engineer. */ "Migrate device" = "Zmigruj urządzenie"; -/* No comment provided by engineer. */ -"Migrate from another device" = "Zmigruj z innego urządzenia"; - /* No comment provided by engineer. */ "Migrate here" = "Zmigruj tutaj"; @@ -3754,7 +3741,10 @@ snd error text */ "No unread chats" = "Brak nieprzeczytanych czatów"; /* No comment provided by engineer. */ -"No user identifiers." = "Brak identyfikatorów użytkownika."; +"Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "Nikt nie śledził twoich rozmów. Nikt nie rysował mapy miejsc, w których byłeś. Prywatność nigdy nie była funkcją - była sposobem na życie."; + +/* No comment provided by engineer. */ +"Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "Nie chodzi o lepszy zamek w drzwiach kogoś innego. Nie chodzi o milszego właściciela, który szanuje twoją prywatność, ale nadal prowadzi rejestr wszystkich odwiedzających. Nie jesteś gościem. Jesteś w domu. Żaden król nie może do niego wejść - jesteś suwerenem."; /* No comment provided by engineer. */ "Not compatible!" = "Nie kompatybilny!"; @@ -3812,7 +3802,7 @@ alert button new chat action */ "Ok" = "Ok"; -/* No comment provided by engineer. */ +/* alert button */ "OK" = "OK"; /* No comment provided by engineer. */ @@ -3893,7 +3883,8 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Tylko Twój kontakt może wysyłać wiadomości głosowe."; -/* alert action */ +/* alert action +alert button */ "Open" = "Otwórz"; /* No comment provided by engineer. */ @@ -4148,12 +4139,6 @@ new chat action */ /* No comment provided by engineer. */ "Privacy policy and conditions of use." = "Polityka prywatności i warunki korzystania."; -/* No comment provided by engineer. */ -"Privacy redefined" = "Redefinicja prywatności"; - -/* No comment provided by engineer. */ -"Private chats, groups and your contacts are not accessible to server operators." = "Prywatne czaty, grupy i Twoje kontakty nie są dostępne dla operatorów serwerów."; - /* No comment provided by engineer. */ "Private filenames" = "Prywatne nazwy plików"; @@ -4193,9 +4178,6 @@ new chat action */ /* No comment provided by engineer. */ "Profile theme" = "Motyw profilu"; -/* alert message */ -"Profile update will be sent to your contacts." = "Aktualizacja profilu zostanie wysłana do Twoich kontaktów."; - /* No comment provided by engineer. */ "Prohibit audio/video calls." = "Zabroń połączeń audio/wideo."; @@ -4284,16 +4266,10 @@ new chat action */ "Read more" = "Przeczytaj więcej"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Przeczytaj więcej w [Poradniku Użytkownika](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in our GitHub repository." = "Przeczytaj więcej na naszym repozytorium GitHub."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; - -/* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Przeczytaj więcej w [Podręczniku Użytkownika](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)." = "Przeczytaj więcej na naszym [repozytorium GitHub](https://github.com/simplex-chat/simplex-chat#readme)."; +"Read more in User Guide." = "Przeczytaj więcej w Poradniku Użytkownika."; /* No comment provided by engineer. */ "Receipts are disabled" = "Potwierdzenia są wyłączone"; @@ -5040,9 +5016,6 @@ chat item action */ /* No comment provided by engineer. */ "Share address publicly" = "Udostępnij adres publicznie"; -/* alert title */ -"Share address with contacts?" = "Udostępnić adres kontaktom?"; - /* No comment provided by engineer. */ "Share from other apps." = "Udostępnij z innych aplikacji."; @@ -5067,9 +5040,6 @@ chat item action */ /* No comment provided by engineer. */ "Share to SimpleX" = "Udostępnij do SimpleX"; -/* No comment provided by engineer. */ -"Share with contacts" = "Udostępnij kontaktom"; - /* No comment provided by engineer. */ "Share your address" = "Udostępnij swój adres"; @@ -5172,9 +5142,6 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Protokoły SimpleX sprawdzone przez Trail of Bits."; -/* simplex link type */ -"SimpleX relay link" = "łącze przekaźnikowe SimpleX"; - /* No comment provided by engineer. */ "Simplified incognito mode" = "Uproszczony tryb incognito"; @@ -5227,6 +5194,9 @@ report reason */ /* chat item text */ "standard end-to-end encryption" = "standardowe szyfrowanie end-to-end"; +/* No comment provided by engineer. */ +"Star on GitHub" = "Daj gwiazdkę na GitHub"; + /* No comment provided by engineer. */ "Start chat" = "Rozpocznij czat"; @@ -5332,9 +5302,6 @@ report reason */ /* No comment provided by engineer. */ "Tap Connect to use bot" = "Dotknij Połącz aby użyć bota"; -/* No comment provided by engineer. */ -"Tap Create SimpleX address in the menu to create it later." = "Dotknij Stwórz adres SimpleX w menu aby utworzyć go później."; - /* No comment provided by engineer. */ "Tap Join group" = "Dotknij Dołącz do grupy"; @@ -5380,7 +5347,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "Tymczasowy błąd pliku"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Test nie powiódł się na etapie %@."; /* No comment provided by engineer. */ @@ -5437,9 +5405,6 @@ report reason */ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Szyfrowanie działa, a nowe uzgodnienie szyfrowania nie jest wymagane. Może to spowodować błędy w połączeniu!"; -/* No comment provided by engineer. */ -"The future of messaging" = "Następna generacja prywatnych wiadomości"; - /* No comment provided by engineer. */ "The hash of the previous message is different." = "Hash poprzedniej wiadomości jest inny."; @@ -5464,6 +5429,9 @@ report reason */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Stara baza danych nie została usunięta podczas migracji, można ją usunąć."; +/* No comment provided by engineer. */ +"The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it." = "Najstarsza ludzka wolność - możliwość rozmowy z inną osobą bez bycia obserwowanym - opiera się na infrastrukturze, która nie może jej zdradzić."; + /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Te same warunki będą miały zastosowanie do operatora **%@**."; @@ -5491,6 +5459,12 @@ report reason */ /* No comment provided by engineer. */ "Themes" = "Motywy"; +/* No comment provided by engineer. */ +"Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible." = "Następnie przenieśliśmy się do sieci, a każda platforma prosiła o podanie danych osobowych - imienia i nazwiska, numeru telefonu, znajomych. Zaakceptowaliśmy fakt, że ceną za możliwość komunikowania się z innymi jest ujawnienie komuś, z kim rozmawiamy. Tak było w przypadku każdego pokolenia, ludzi i technologii - telefonu, poczty elektronicznej, komunikatorów, mediów społecznościowych. Wydawało się to jedyną możliwą opcją."; + +/* No comment provided by engineer. */ +"There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected." = "Jest jeszcze inny sposób. Sieć bez numerów telefonów. Bez nazw użytkowników. Bez kont. Bez jakichkolwiek tożsamości użytkowników. Sieć, która łączy ludzi i przesyła zaszyfrowane wiadomości, nie wiedząc, kto jest podłączony."; + /* No comment provided by engineer. */ "These conditions will also apply for: **%@**." = "Warunki te będą miały również zastosowanie w przypadku: **%@**."; @@ -5614,9 +5588,6 @@ report reason */ /* 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 chat list:" = "Przełącz listę czatów:"; - /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Przełącz incognito przy połączeniu."; @@ -5737,7 +5708,7 @@ report reason */ /* swipe action */ "Unread" = "Nieprzeczytane"; -/* No comment provided by engineer. */ +/* conn error description */ "Unsupported connection link" = "Nieobsługiwane łącze połączenia"; /* No comment provided by engineer. */ @@ -5812,9 +5783,6 @@ report reason */ /* No comment provided by engineer. */ "Use %@" = "Użyj %@"; -/* No comment provided by engineer. */ -"Use chat" = "Użyj czatu"; - /* new chat action */ "Use current profile" = "Użyj obecnego profilu"; @@ -6232,9 +6200,6 @@ report reason */ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Nie można zweryfikować użytkownika; proszę spróbować ponownie."; -/* No comment provided by engineer. */ -"You decide who can connect." = "Ty decydujesz, kto może się połączyć."; - /* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Już prosiłeś o połączenie!\nPowtórzyć prośbę połączenia?"; @@ -6289,6 +6254,9 @@ report reason */ /* snd group event chat item */ "you unblocked %@" = "odblokowałeś %@"; +/* No comment provided by engineer. */ +"You were born without an account" = "Urodziłeś się bez konta."; + /* No comment provided by engineer. */ "You will be able to send messages **only after your request is accepted**." = "Będziesz mógł wysyłać wiadomości **dopiero po zaakceptowaniu Twojej prośby**."; @@ -6364,6 +6332,9 @@ report reason */ /* No comment provided by engineer. */ "Your contacts will remain connected." = "Twoje kontakty pozostaną połączone."; +/* No comment provided by engineer. */ +"Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public." = "Twoje rozmowy należą do Ciebie, tak jak zawsze było przed pojawieniem się Internetu. Sieć nie jest miejscem, które odwiedzasz. Jest miejscem, które tworzysz i które należy do Ciebie. Nikt nie może Ci tego odebrać, niezależnie od tego, czy jest to miejsce prywatne, czy publiczne."; + /* No comment provided by engineer. */ "Your credentials may be sent unencrypted." = "Twoje poświadczenia mogą zostać wysłane niezaszyfrowane."; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 87a47ec2ab..c2b01228a2 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -5,11 +5,14 @@ "_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- быстрее и стабильнее."; +"- 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- и прочее!"; +/* No comment provided by engineer. */ +"- opt-in to send link previews.\n- prevent hyperlink phishing.\n- remove link tracking." = "- включение картинок ссылок.\n- защита от фишинга.\n- удаление трекинга ссылок."; + /* No comment provided by engineer. */ "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- опционально уведомляйте удалённые контакты.\n- имена профилей с пробелами.\n- и прочее!"; @@ -19,21 +22,21 @@ /* No comment provided by engineer. */ "!1 colored!" = "!1 цвет!"; +/* chat link info line */ +"(from owner)" = "(от владельца)"; + /* No comment provided by engineer. */ "(new)" = "(новое)"; +/* chat link info line */ +"(signed)" = "(с подписью)"; + /* No comment provided by engineer. */ "(this device v%@)" = "(это устройство v%@)"; -/* 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)" = "[Отправить email](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. */ "**Create 1-time link**: to create and share a new invitation link." = "**Добавить контакт**: создать и поделиться новой ссылкой-приглашением."; @@ -56,7 +59,7 @@ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Обратите внимание**: использование одной и той же базы данных на двух устройствах нарушит расшифровку сообщений от ваших контактов, как свойство защиты соединений."; /* No comment provided by engineer. */ -"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Внимание**: Вы не сможете восстановить или поменять пароль, если Вы его потеряете."; +"**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 end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Рекомендовано**: токен устройства и уведомления отправляются на сервер SimpleX Chat, но сервер не получает сами сообщения, их размер или от кого они."; @@ -65,10 +68,13 @@ "**Scan / Paste link**: to connect via a link you received." = "**Сканировать / Вставить ссылку**: чтобы соединиться через полученную ссылку."; /* No comment provided by engineer. */ -"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Внимание**: для работы мгновенных уведомлений пароль должен быть сохранен в Keychain."; +"**Test relay** to retrieve its name." = "**Протестируйте релей**, чтобы получить его имя."; /* No comment provided by engineer. */ -"**Warning**: the archive will be removed." = "**Внимание**: архив будет удален."; +"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Внимание**: для работы мгновенных уведомлений пароль должен быть сохранён в Keychain."; + +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Внимание**: архив будет удалён."; /* No comment provided by engineer. */ "*bold*" = "\\*жирный*"; @@ -164,7 +170,7 @@ "%d file(s) were not downloaded." = "%d файлов не было загружено."; /* time interval */ -"%d hours" = "%d ч."; +"%d hours" = "%d ч"; /* alert title */ "%d messages not forwarded" = "%d сообщений не переслано"; @@ -173,7 +179,19 @@ "%d min" = "%d мин"; /* time interval */ -"%d months" = "%d мес."; +"%d months" = "%d мес"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays failed" = "%d релеев с ошибками"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays not active" = "%d релеев неактивны"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays removed" = "%d релеев удалены"; /* time interval */ "%d sec" = "%d сек"; @@ -184,15 +202,50 @@ /* integrity error chat item */ "%d skipped message(s)" = "%d пропущенных сообщение(й)"; +/* channel subscriber count */ +"%d subscriber" = "%d подписчик"; + +/* channel subscriber count */ +"%d subscribers" = "%d подписчиков"; + /* time interval */ "%d weeks" = "%d недель"; +/* channel creation progress +channel relay bar progress */ +"%d/%d relays active" = "%1$d/%2$d релеев активны"; + +/* channel relay bar */ +"%d/%d relays active, %d errors" = "%1$d/%2$d релеев активны, %3$d с ошибками"; + +/* channel creation progress with errors +channel relay bar */ +"%d/%d relays active, %d failed" = "%1$d/%2$d релеев активны, %3$d с ошибками"; + +/* channel relay bar */ +"%d/%d relays active, %d removed" = "%1$d/%2$d релеев активны, %3$d удалены"; + +/* channel subscriber relay bar progress */ +"%d/%d relays connected" = "%1$d/%2$d релеев подключены"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d errors" = "%1$d/%2$d релеев подключены, %3$d с ошибками"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d failed" = "%1$d/%2$d релеев подключены, %3$d с ошибками"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d removed" = "%1$d/%2$d релеев подключены, %3$d удалены"; + /* No comment provided by engineer. */ "%lld" = "%lld"; /* No comment provided by engineer. */ "%lld %@" = "%lld %@"; +/* No comment provided by engineer. */ +"%lld channel events" = "%lld событий канала"; + /* No comment provided by engineer. */ "%lld contact(s) selected" = "Выбрано контактов: %lld"; @@ -209,7 +262,7 @@ "%lld messages blocked" = "%lld сообщений заблокировано"; /* No comment provided by engineer. */ -"%lld messages blocked by admin" = "%lld сообщений заблокировано администратором"; +"%lld messages blocked by admin" = "%lld сообщений заблокировано админом"; /* No comment provided by engineer. */ "%lld messages marked deleted" = "%lld сообщений помечено удалёнными"; @@ -218,7 +271,7 @@ "%lld messages moderated by %@" = "%lld сообщений модерировано членом %@"; /* No comment provided by engineer. */ -"%lld minutes" = "%lld минуты"; +"%lld minutes" = "%lld минут(ы)"; /* No comment provided by engineer. */ "%lld new interface languages" = "%lld новых языков интерфейса"; @@ -262,6 +315,9 @@ /* No comment provided by engineer. */ "~strike~" = "\\~зачеркнуть~"; +/* owner verification */ +"⚠️ Signature verification failed: %@." = "⚠️ Ошибка проверки подписи: %@."; + /* time to disappear */ "0 sec" = "0 сек"; @@ -305,7 +361,10 @@ time interval */ "30 seconds" = "30 секунд"; /* No comment provided by engineer. */ -"A few more things" = "Еще несколько изменений"; +"A few more things" = "Ещё несколько изменений"; + +/* No comment provided by engineer. */ +"A link for one person to connect" = "Ссылка для одного человека"; /* notification title */ "A new contact" = "Новый контакт"; @@ -317,7 +376,7 @@ time interval */ "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**Примечание**: Чем больше подключений, тем быстрее разряжается батарея и расходуется трафик, а некоторые соединения могут отваливаться."; +"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" = "Прекратить"; @@ -369,7 +428,10 @@ swipe action */ "Accept incognito" = "Принять инкогнито"; /* alert title */ -"Accept member" = "Принять члена"; +"Accept member" = "Принять члена группы"; + +/* No comment provided by engineer. */ +"accepted" = "принят(а)"; /* rcv group event chat item */ "accepted %@" = "принят %@"; @@ -392,6 +454,9 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledgement errors" = "Ошибки подтверждения"; +/* No comment provided by engineer. */ +"active" = "активный"; + /* token status text */ "Active" = "Активный"; @@ -399,7 +464,7 @@ swipe action */ "Active connections" = "Активные соединения"; /* 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." = "Добавьте адрес в свой профиль, чтобы Ваши контакты могли поделиться им. Профиль будет отправлен Вашим контактам."; +"Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts." = "Добавьте адрес в свой профиль, чтобы Ваши SimpleX контакты могли поделиться им. Профиль будет отправлен Вашим SimpleX контактам."; /* No comment provided by engineer. */ "Add friends" = "Добавить друзей"; @@ -408,7 +473,7 @@ swipe action */ "Add list" = "Добавить список"; /* placeholder for sending contact request */ -"Add message" = "Добавить cообщение"; +"Add message" = "Добавить сообщение"; /* No comment provided by engineer. */ "Add profile" = "Добавить профиль"; @@ -417,7 +482,7 @@ swipe action */ "Add server" = "Добавить сервер"; /* No comment provided by engineer. */ -"Add servers by scanning QR codes." = "Добавить серверы через QR код."; +"Add servers by scanning QR codes." = "Добавить серверы через QR-код."; /* No comment provided by engineer. */ "Add team members" = "Добавить сотрудников"; @@ -440,6 +505,9 @@ swipe action */ /* No comment provided by engineer. */ "Added message servers" = "Дополнительные серверы сообщений"; +/* No comment provided by engineer. */ +"Adding relays will be supported later." = "Добавление релеев будет поддерживаться позже."; + /* No comment provided by engineer. */ "Additional accent" = "Дополнительный акцент"; @@ -477,7 +545,7 @@ swipe action */ "Advanced network settings" = "Настройки сети"; /* No comment provided by engineer. */ -"Advanced settings" = "Настройки сети"; +"Advanced settings" = "Дополнительные настройки"; /* chat item text */ "agreeing encryption for %@…" = "шифрование согласовывается для %@…"; @@ -498,7 +566,7 @@ swipe action */ "All chats and messages will be deleted - this cannot be undone!" = "Все чаты и сообщения будут удалены - это нельзя отменить!"; /* alert message */ -"All chats will be removed from the list %@, and the list deleted." = "Все чаты будут удалены из списка %@, и список удален."; +"All chats will be removed from the list %@, and the list deleted." = "Все чаты будут удалены из списка %@, и список удалён."; /* No comment provided by engineer. */ "All data is erased when it is entered." = "Все данные удаляются при его вводе."; @@ -513,7 +581,10 @@ swipe action */ "all members" = "все члены"; /* No comment provided by engineer. */ -"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Все сообщения и файлы отправляются с **end-to-end шифрованием**, с постквантовой безопасностью в прямых разговорах."; +"All messages" = "Все сообщения"; + +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Все сообщения и файлы отправляются с **сквозным шифрованием**, с пост-квантовой безопасностью в прямых разговорах."; /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Все сообщения будут удалены - это нельзя отменить!"; @@ -527,6 +598,12 @@ swipe action */ /* profile dropdown */ "All profiles" = "Все профили"; +/* No comment provided by engineer. */ +"All relays failed" = "Все релеи недоступны"; + +/* No comment provided by engineer. */ +"All relays removed" = "Все релеи удалены"; + /* No comment provided by engineer. */ "All reports will be archived for you." = "Все сообщения о нарушениях будут заархивированы для вас."; @@ -537,10 +614,10 @@ swipe action */ "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." = "Все Ваши контакты сохранятся. Обновленный профиль будет отправлен Вашим контактам."; +"All your contacts will remain connected. Profile update will be sent to your contacts." = "Все Ваши контакты сохранятся. Обновлённый профиль будет отправлен Вашим контактам."; /* No comment provided by engineer. */ -"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Все ваши контакты, разговоры и файлы будут надежно зашифрованы и загружены на выбранные XFTP серверы."; +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Все ваши контакты, разговоры и файлы будут надёжно зашифрованы и загружены на выбранные XFTP-серверы."; /* No comment provided by engineer. */ "Allow" = "Разрешить"; @@ -563,6 +640,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам. (24 часа)"; +/* No comment provided by engineer. */ +"Allow members to chat with admins." = "Разрешить членам группы общаться с админами."; + /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "Разрешить реакции на сообщения, только если ваш контакт разрешает их."; @@ -572,12 +652,18 @@ swipe action */ /* No comment provided by engineer. */ "Allow sending direct messages to members." = "Разрешить личные сообщения членам группы."; +/* No comment provided by engineer. */ +"Allow sending direct messages to subscribers." = "Разрешить отправку личных сообщений подписчикам."; + /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Разрешить посылать исчезающие сообщения."; /* No comment provided by engineer. */ "Allow sharing" = "Разрешить поделиться"; +/* No comment provided by engineer. */ +"Allow subscribers to chat with admins." = "Разрешить подписчикам общаться с админами."; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Разрешить необратимо удалять отправленные сообщения. (24 часа)"; @@ -633,7 +719,7 @@ swipe action */ "Always use private routing." = "Всегда использовать конфиденциальную доставку."; /* No comment provided by engineer. */ -"Always use relay" = "Всегда соединяться через relay"; +"Always use relay" = "Всегда соединяться через релей"; /* No comment provided by engineer. */ "An empty chat profile with the provided name is created, and the app opens as usual." = "Будет создан пустой профиль чата с указанным именем, и приложение откроется в обычном режиме."; @@ -647,9 +733,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Принять звонок"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "Кто угодно может запустить сервер."; - /* No comment provided by engineer. */ "App build: %@" = "Сборка приложения: %@"; @@ -669,7 +752,7 @@ swipe action */ "App passcode" = "Код доступа в приложение"; /* No comment provided by engineer. */ -"App passcode is replaced with self-destruct passcode." = "Код доступа в приложение будет заменен кодом самоуничтожения."; +"App passcode is replaced with self-destruct passcode." = "Код доступа в приложение будет заменён кодом самоуничтожения."; /* No comment provided by engineer. */ "App session" = "Сессия приложения"; @@ -734,6 +817,9 @@ swipe action */ /* No comment provided by engineer. */ "Audio and video calls" = "Аудио и видео звонки"; +/* No comment provided by engineer. */ +"Audio call" = "Аудиозвонок"; + /* No comment provided by engineer. */ "audio call (not e2e encrypted)" = "аудиозвонок (не e2e зашифрованный)"; @@ -759,13 +845,13 @@ swipe action */ "author" = "автор"; /* No comment provided by engineer. */ -"Auto-accept" = "Автоприем"; +"Auto-accept" = "Автоприём"; /* No comment provided by engineer. */ "Auto-accept contact requests" = "Автоматически принимать запросы контактов"; /* No comment provided by engineer. */ -"Auto-accept images" = "Автоприем изображений"; +"Auto-accept images" = "Автоприём изображений"; /* No comment provided by engineer. */ "Back" = "Назад"; @@ -777,10 +863,10 @@ swipe action */ "Bad desktop address" = "Неверный адрес компьютера"; /* integrity error chat item */ -"bad message hash" = "ошибка хэш сообщения"; +"bad message hash" = "ошибка хэша сообщения"; /* No comment provided by engineer. */ -"Bad message hash" = "Ошибка хэш сообщения"; +"Bad message hash" = "Ошибка хэша сообщения"; /* integrity error chat item */ "bad message ID" = "ошибка ID сообщения"; @@ -788,6 +874,15 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "Ошибка ID сообщения"; +/* No comment provided by engineer. */ +"Be free\nin your network" = "Будь свободен\nв своей сети"; + +/* No comment provided by engineer. */ +"Be free in your network." = "Будь свободен в своей сети."; + +/* No comment provided by engineer. */ +"Because we destroyed the power to know who you are. So that your power can never be taken." = "Потому что мы разрушили саму возможность узнать, кто вы. Чтобы вашу свободу невозможно было отнять."; + /* No comment provided by engineer. */ "Better calls" = "Улучшенные звонки"; @@ -825,7 +920,7 @@ swipe action */ "Bio too large" = "Описание слишком длинное"; /* No comment provided by engineer. */ -"Black" = "Черная"; +"Black" = "Чёрная"; /* No comment provided by engineer. */ "Block" = "Заблокировать"; @@ -845,6 +940,9 @@ swipe action */ /* No comment provided by engineer. */ "Block member?" = "Заблокировать члена группы?"; +/* No comment provided by engineer. */ +"Block subscriber for all?" = "Заблокировать подписчика для всех?"; + /* marked deleted chat item preview text */ "blocked" = "заблокировано"; @@ -889,16 +987,22 @@ marked deleted chat item preview text */ "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)!"; +"Bottom bar" = "Нижнее меню"; + +/* compose placeholder for channel owner */ +"Broadcast" = "Опубликовать"; /* No comment provided by engineer. */ -"Business address" = "Бизнес адрес"; +"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)!"; + +/* chat link info line */ +"Business address" = "Бизнес-адрес"; /* No comment provided by engineer. */ "Business chats" = "Бизнес разговоры"; /* No comment provided by engineer. */ -"Business connection" = "Бизнес контакт"; +"Business connection" = "Бизнес-контакт"; /* No comment provided by engineer. */ "Businesses" = "Бизнесы"; @@ -906,14 +1010,11 @@ marked deleted chat item preview text */ /* 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. */ -"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "Используя SimpleX Chat, Вы согласны:\n- отправлять только законные сообщения в публичных группах.\n- уважать других пользователей – не отправлять спам."; - /* No comment provided by engineer. */ "call" = "звонок"; /* No comment provided by engineer. */ -"Call already ended!" = "Звонок уже завершен!"; +"Call already ended!" = "Звонок уже завершён!"; /* call status */ "call error" = "ошибка звонка"; @@ -934,7 +1035,10 @@ marked deleted chat item preview text */ "Camera not available" = "Камера недоступна"; /* No comment provided by engineer. */ -"Can't call contact" = "Не удается позвонить контакту"; +"can't broadcast" = "нельзя публиковать"; + +/* No comment provided by engineer. */ +"Can't call contact" = "Не удаётся позвонить контакту"; /* No comment provided by engineer. */ "Can't call member" = "Не удаётся позвонить члену группы"; @@ -987,7 +1091,7 @@ new chat action */ "Change automatic message deletion?" = "Измененить автоматическое удаление сообщений?"; /* authentication reason */ -"Change chat profiles" = "Поменять профили"; +"Change chat profiles" = "Изменить профили чата"; /* No comment provided by engineer. */ "Change database passphrase?" = "Поменять пароль базы данных?"; @@ -1032,6 +1136,58 @@ set passcode view */ /* chat item text */ "changing address…" = "смена адреса…"; +/* shown as sender role for channel messages */ +"channel" = "канал"; + +/* No comment provided by engineer. */ +"Channel" = "Канал"; + +/* No comment provided by engineer. */ +"Channel display name" = "Имя канала"; + +/* No comment provided by engineer. */ +"Channel full name (optional)" = "Полное имя канала (необязательно)"; + +/* alert message +alert subtitle */ +"Channel has no active relays. Please try to join later." = "У канала нет активных релеев. Попробуйте подключиться позже."; + +/* No comment provided by engineer. */ +"Channel image" = "Картинка канала"; + +/* chat link info line */ +"Channel link" = "Ссылка канала"; + +/* No comment provided by engineer. */ +"Channel preferences" = "Предпочтения канала"; + +/* No comment provided by engineer. */ +"Channel profile" = "Профиль канала"; + +/* No comment provided by engineer. */ +"Channel profile is stored on subscribers' devices and on the chat relays." = "Профиль канала хранится на устройствах подписчиков и на чат-релеях."; + +/* snd group event chat item */ +"channel profile updated" = "профиль канала обновлён"; + +/* alert message */ +"Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers." = "Профиль канала был изменен. Если Вы сохраните его, обновлённый профиль будет отправлен подписчикам канала."; + +/* alert title */ +"Channel temporarily unavailable" = "Канал временно недоступен"; + +/* No comment provided by engineer. */ +"Channel will be deleted for all subscribers - this cannot be undone!" = "Канал будет удалён для всех подписчиков - это нельзя отменить!"; + +/* No comment provided by engineer. */ +"Channel will be deleted for you - this cannot be undone!" = "Канал будет удалён для Вас - это нельзя отменить!"; + +/* alert message */ +"Channel will start working with %d of %d relays. Proceed?" = "Канал начнёт работу с %1$d из %2$d релеев. Продолжить?"; + +/* No comment provided by engineer. */ +"Channels" = "Каналы"; + /* No comment provided by engineer. */ "Chat" = "Разговор"; @@ -1066,7 +1222,7 @@ set passcode view */ "Chat is stopped" = "Чат остановлен"; /* No comment provided by engineer. */ -"Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "Чат остановлен. Если вы уже использовали эту базу данных на другом устройстве, перенесите ее обратно до запуска чата."; +"Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "Чат остановлен. Если вы уже использовали эту базу данных на другом устройстве, перенесите её обратно до запуска чата."; /* No comment provided by engineer. */ "Chat list" = "Список чатов"; @@ -1075,7 +1231,7 @@ set passcode view */ "Chat migrated!" = "Чат мигрирован!"; /* No comment provided by engineer. */ -"Chat preferences" = "Предпочтения"; +"Chat preferences" = "Настройки чатов"; /* alert message */ "Chat preferences were changed." = "Настройки чата были изменены."; @@ -1083,36 +1239,64 @@ set passcode view */ /* No comment provided by engineer. */ "Chat profile" = "Профиль чата"; +/* No comment provided by engineer. */ +"Chat relay" = "Чат-релей"; + +/* No comment provided by engineer. */ +"Chat relays" = "Чат-релеи"; + +/* No comment provided by engineer. */ +"Chat relays forward messages in channels you create." = "Чат-релеи пересылают сообщения в Ваших каналах."; + +/* No comment provided by engineer. */ +"Chat relays forward messages to channel subscribers." = "Чат-релеи пересылают сообщения подписчикам каналов."; + /* No comment provided by engineer. */ "Chat theme" = "Тема чата"; /* No comment provided by engineer. */ -"Chat will be deleted for all members - this cannot be undone!" = "Разговор будет удален для всех участников - это действие нельзя отменить!"; +"Chat will be deleted for all members - this cannot be undone!" = "Разговор будет удалён для всех участников - это действие нельзя отменить!"; /* No comment provided by engineer. */ -"Chat will be deleted for you - this cannot be undone!" = "Разговор будет удален для Вас - это действие нельзя отменить!"; +"Chat will be deleted for you - this cannot be undone!" = "Разговор будет удалён для Вас - это действие нельзя отменить!"; -/* chat toolbar */ +/* chat feature +chat toolbar */ "Chat with admins" = "Чат с админами"; /* No comment provided by engineer. */ "Chat with member" = "Чат с членом группы"; /* No comment provided by engineer. */ -"Chat with members before they join." = "Общайтесь с членами до того как принять их."; +"Chat with members before they join." = "Общайтесь с членами группы до того как принять их."; /* No comment provided by engineer. */ "Chats" = "Чаты"; +/* No comment provided by engineer. */ +"Chats with admins are prohibited." = "Чаты с админами запрещены."; + +/* alert message */ +"Chats with admins in public channels have no E2E encryption - use only with trusted chat relays." = "Чаты с админами в публичных каналах не имеют E2E шифрования - используйте только с доверенными чат-релеями."; + /* No comment provided by engineer. */ "Chats with members" = "Чаты с членами группы"; +/* No comment provided by engineer. */ +"Chats with members are disabled" = "Чаты с членами группы отключены"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Проверять сообщения каждые 20 минут."; /* No comment provided by engineer. */ "Check messages when allowed." = "Проверять сообщения по возможности."; +/* alert message */ +"Check relay address and try again." = "Проверьте адрес релея и попробуйте снова."; + +/* alert message */ +"Check relay name and try again." = "Проверьте имя релея и попробуйте снова."; + /* alert title */ "Check server address and try again." = "Проверьте адрес сервера и попробуйте снова."; @@ -1120,7 +1304,7 @@ set passcode view */ "Chinese and Spanish interface" = "Китайский и Испанский интерфейс"; /* No comment provided by engineer. */ -"Choose _Migrate from another device_ on the new device and scan QR code." = "Выберите _Мигрировать с другого устройства_ на новом устройстве и сосканируйте QR код."; +"Choose _Migrate from another device_ on the new device and scan QR code." = "Выберите _Мигрировать с другого устройства_ на новом устройстве и сосканируйте QR-код."; /* No comment provided by engineer. */ "Choose file" = "Выбрать файл"; @@ -1204,10 +1388,10 @@ set passcode view */ "Conditions will be automatically accepted for enabled operators on: %@." = "Условия будут автоматически приняты для включенных операторов: %@."; /* No comment provided by engineer. */ -"Configure ICE servers" = "Настройка ICE серверов"; +"Configure ICE servers" = "Настройка ICE-серверов"; /* No comment provided by engineer. */ -"Configure server operators" = "Настроить операторов серверов"; +"Configure relays" = "Настроить релеи"; /* No comment provided by engineer. */ "Confirm" = "Подтвердить"; @@ -1234,7 +1418,7 @@ set passcode view */ "Confirm password" = "Подтвердить пароль"; /* No comment provided by engineer. */ -"Confirm that you remember database passphrase to migrate it." = "Подтвердите, что Вы помните пароль базы данных для ее миграции."; +"Confirm that you remember database passphrase to migrate it." = "Подтвердите, что Вы помните пароль базы данных для её миграции."; /* No comment provided by engineer. */ "Confirm upload" = "Подтвердить загрузку"; @@ -1242,7 +1426,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Подтвержденный"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Соединиться"; /* No comment provided by engineer. */ @@ -1272,6 +1457,9 @@ set passcode view */ /* new chat sheet title */ "Connect via link" = "Соединиться через ссылку"; +/* No comment provided by engineer. */ +"Connect via link or QR code" = "Соединитесь по ссылке или QR"; + /* new chat sheet title */ "Connect via one-time link" = "Соединиться через одноразовую ссылку"; @@ -1279,7 +1467,7 @@ set passcode view */ "Connect with %@" = "Соединиться с %@"; /* No comment provided by engineer. */ -"connected" = "соединение установлено"; +"connected" = "соединен(а)"; /* No comment provided by engineer. */ "Connected" = "Соединено"; @@ -1341,12 +1529,15 @@ set passcode view */ /* alert title */ "Connection error" = "Ошибка соединения"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "Ошибка соединения (AUTH)"; /* chat list item title (it should not be shown */ "connection established" = "соединение установлено"; +/* No comment provided by engineer. */ +"Connection failed" = "Ошибка соединения"; + /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "Соединение заблокировано сервером оператора:\n%@"; @@ -1383,6 +1574,9 @@ set passcode view */ /* profile update event chat item */ "contact %@ changed to %@" = "контакт %1$@ изменён на %2$@"; +/* chat link info line */ +"Contact address" = "Адрес контакта"; + /* No comment provided by engineer. */ "Contact allows" = "Контакт разрешает"; @@ -1390,10 +1584,10 @@ set passcode view */ "Contact already exists" = "Существующий контакт"; /* No comment provided by engineer. */ -"contact deleted" = "контакт удален"; +"contact deleted" = "контакт удалён"; /* No comment provided by engineer. */ -"Contact deleted!" = "Контакт удален!"; +"Contact deleted!" = "Контакт удалён!"; /* No comment provided by engineer. */ "contact disabled" = "контакт выключен"; @@ -1411,7 +1605,7 @@ set passcode view */ "Contact is connected" = "Соединение с контактом установлено"; /* No comment provided by engineer. */ -"Contact is deleted." = "Контакт удален."; +"Contact is deleted." = "Контакт удалён."; /* No comment provided by engineer. */ "Contact name" = "Имена контактов"; @@ -1429,7 +1623,7 @@ set passcode view */ "contact should accept…" = "контакт должен принять…"; /* No comment provided by engineer. */ -"Contact will be deleted - this cannot be undone!" = "Контакт будет удален — это нельзя отменить!"; +"Contact will be deleted - this cannot be undone!" = "Контакт будет удалён - это нельзя отменить!"; /* No comment provided by engineer. */ "Contacts" = "Контакты"; @@ -1444,13 +1638,16 @@ set passcode view */ "Continue" = "Продолжить"; /* No comment provided by engineer. */ -"Conversation deleted!" = "Разговор удален!"; +"Contribute" = "Внести свой вклад"; /* No comment provided by engineer. */ -"Copy" = "Скопировать"; +"Conversation deleted!" = "Разговор удалён!"; /* No comment provided by engineer. */ -"Copy error" = "Ошибка копирования"; +"Copy" = "Копировать"; + +/* No comment provided by engineer. */ +"Copy error" = "Скопировать ошибку"; /* No comment provided by engineer. */ "Core version: v%@" = "Версия ядра: v%@"; @@ -1458,12 +1655,9 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Угол"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Исправить имя на %@?"; -/* No comment provided by engineer. */ -"Create" = "Создать"; - /* No comment provided by engineer. */ "Create 1-time link" = "Создать одноразовую ссылку"; @@ -1491,6 +1685,12 @@ set passcode view */ /* No comment provided by engineer. */ "Create profile" = "Создать профиль"; +/* No comment provided by engineer. */ +"Create public channel" = "Создать публичный канал"; + +/* No comment provided by engineer. */ +"Create public channel (BETA)" = "Создать публичный канал (БЕТА)"; + /* server test step */ "Create queue" = "Создание очереди"; @@ -1500,9 +1700,15 @@ set passcode view */ /* No comment provided by engineer. */ "Create your address" = "Создайте Ваш адрес"; +/* No comment provided by engineer. */ +"Create your link" = "Создайте Вашу ссылку"; + /* No comment provided by engineer. */ "Create your profile" = "Создать профиль"; +/* No comment provided by engineer. */ +"Create your public address" = "Создайте Ваш публичный адрес"; + /* No comment provided by engineer. */ "Created" = "Создано"; @@ -1515,6 +1721,9 @@ set passcode view */ /* No comment provided by engineer. */ "Creating archive link" = "Создание ссылки на архив"; +/* No comment provided by engineer. */ +"Creating channel" = "Создание канала"; + /* No comment provided by engineer. */ "Creating link…" = "Создаётся ссылка…"; @@ -1561,7 +1770,7 @@ set passcode view */ "Database encrypted!" = "База данных зашифрована!"; /* No comment provided by engineer. */ -"Database encryption passphrase will be updated and stored in the keychain.\n" = "Пароль базы данных будет изменен и сохранен в Keychain.\n"; +"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"; @@ -1591,10 +1800,10 @@ set passcode view */ "Database passphrase & export" = "Пароль и экспорт базы"; /* No comment provided by engineer. */ -"Database passphrase is different from saved in the keychain." = "Пароль базы данных отличается от сохраненного в Keychain."; +"Database passphrase is different from saved in the keychain." = "Пароль базы данных отличается от сохранённого в Keychain."; /* No comment provided by engineer. */ -"Database passphrase is required to open chat." = "Введите пароль базы данных чтобы открыть чат."; +"Database passphrase is required to open chat." = "Введите пароль базы данных, чтобы открыть чат."; /* No comment provided by engineer. */ "Database upgrade" = "Обновление базы данных"; @@ -1603,7 +1812,7 @@ set passcode view */ "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"; +"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"; @@ -1617,8 +1826,8 @@ set passcode view */ /* No comment provided by engineer. */ "Debug delivery" = "Отладка доставки"; -/* No comment provided by engineer. */ -"Decentralized" = "Децентрализованный"; +/* relay test step */ +"Decode link" = "Расшифровать ссылку"; /* message decrypt error item */ "Decryption error" = "Ошибка расшифровки"; @@ -1661,6 +1870,12 @@ swipe action */ /* No comment provided by engineer. */ "Delete and notify contact" = "Удалить и уведомить контакт"; +/* No comment provided by engineer. */ +"Delete channel" = "Удалить канал"; + +/* No comment provided by engineer. */ +"Delete channel?" = "Удалить канал?"; + /* No comment provided by engineer. */ "Delete chat" = "Удалить разговор"; @@ -1704,7 +1919,7 @@ swipe action */ "Delete files for all chat profiles" = "Удалить файлы во всех профилях чата"; /* chat feature */ -"Delete for everyone" = "Удалить для всех"; +"Delete for everyone" = "Удаление для всех"; /* No comment provided by engineer. */ "Delete for me" = "Удалить для меня"; @@ -1728,7 +1943,13 @@ swipe action */ "Delete list?" = "Удалить список?"; /* No comment provided by engineer. */ -"Delete member message?" = "Удалить сообщение участника?"; +"Delete member message?" = "Удалить сообщение члена группы\\?"; + +/* No comment provided by engineer. */ +"Delete member messages" = "Удалить сообщения члена группы"; + +/* alert title */ +"Delete member messages?" = "Удалить сообщения члена группы?"; /* No comment provided by engineer. */ "Delete message?" = "Удалить сообщение?"; @@ -1758,6 +1979,9 @@ alert button */ /* server test step */ "Delete queue" = "Удаление очереди"; +/* No comment provided by engineer. */ +"Delete relay" = "Удалить релей"; + /* No comment provided by engineer. */ "Delete report" = "Удалить сообщение о нарушении"; @@ -1782,6 +2006,9 @@ alert button */ /* copied message info */ "Deleted at: %@" = "Удалено: %@"; +/* rcv group event chat item */ +"deleted channel" = "удалил(а) канал"; + /* rcv direct event chat item */ "deleted contact" = "удалил(а) контакт"; @@ -1867,10 +2094,16 @@ alert button */ "Direct messages" = "Прямые сообщения"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this chat." = "Личные сообщения запрещены в этой группе."; +"Direct messages between members are prohibited in this chat." = "Прямые сообщения между членами группы запрещены."; /* No comment provided by engineer. */ -"Direct messages between members are prohibited." = "Прямые сообщения между членами запрещены."; +"Direct messages between members are prohibited." = "Прямые сообщения между членами группы запрещены."; + +/* No comment provided by engineer. */ +"Direct messages between subscribers are prohibited." = "Прямые сообщения между подписчиками запрещены."; + +/* alert button */ +"Disable" = "Выключить"; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Выключить (кроме исключений)"; @@ -1888,7 +2121,7 @@ alert button */ "Disable SimpleX Lock" = "Отключить блокировку SimpleX"; /* No comment provided by engineer. */ -"disabled" = "выключено"; +"disabled" = "выключен"; /* No comment provided by engineer. */ "Disabled" = "Выключено"; @@ -1903,7 +2136,7 @@ alert button */ "Disappearing messages are prohibited in this chat." = "Исчезающие сообщения запрещены в этом чате."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited." = "Исчезающие сообщения запрещены в этой группе."; +"Disappearing messages are prohibited." = "Исчезающие сообщения запрещены."; /* No comment provided by engineer. */ "Disappears at" = "Исчезает"; @@ -1912,7 +2145,7 @@ alert button */ "Disappears at: %@" = "Исчезает: %@"; /* server test step */ -"Disconnect" = "Разрыв соединения"; +"Disconnect" = "Отключить"; /* No comment provided by engineer. */ "Disconnect desktop?" = "Отключить компьютер?"; @@ -1929,11 +2162,14 @@ alert button */ /* No comment provided by engineer. */ "Do not send history to new members." = "Не отправлять историю новым членам."; +/* No comment provided by engineer. */ +"Do not send history to new subscribers." = "Не отправлять историю новым подписчикам."; + /* No comment provided by engineer. */ "Do NOT send messages directly, even if your or destination server does not support private routing." = "Не отправлять сообщения напрямую, даже если сервер получателя не поддерживает конфиденциальную доставку."; /* No comment provided by engineer. */ -"Do not use credentials with proxy." = "Не использовать учетные данные с прокси."; +"Do not use credentials with proxy." = "Не использовать учётные данные с прокси."; /* No comment provided by engineer. */ "Do NOT use private routing." = "Не использовать конфиденциальную доставку."; @@ -1967,7 +2203,7 @@ chat item action */ "Download" = "Загрузить"; /* No comment provided by engineer. */ -"Download errors" = "Ошибки приема"; +"Download errors" = "Ошибки приёма"; /* No comment provided by engineer. */ "Download failed" = "Ошибка загрузки"; @@ -2008,27 +2244,39 @@ chat item action */ /* No comment provided by engineer. */ "E2E encrypted notifications." = "E2E зашифрованные нотификации."; +/* No comment provided by engineer. */ +"Easier to invite your friends 👋" = "Проще пригласить друзей 👋"; + /* chat item action */ "Edit" = "Редактировать"; +/* No comment provided by engineer. */ +"Edit channel profile" = "Редактировать профиль канала"; + /* No comment provided by engineer. */ "Edit group profile" = "Редактировать профиль группы"; /* No comment provided by engineer. */ "Empty message!" = "Пустое сообщение!"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "Включить"; /* No comment provided by engineer. */ "Enable (keep overrides)" = "Включить (кроме исключений)"; +/* channel creation warning */ +"Enable at least one chat relay in Network & Servers." = "Включите хотя бы один чат-релей в настройках Сеть и серверы."; + /* alert title */ "Enable automatic message deletion?" = "Включить автоматическое удаление сообщений?"; /* No comment provided by engineer. */ "Enable camera access" = "Включить доступ к камере"; +/* alert title */ +"Enable chats with admins?" = "Включить чаты с админами?"; + /* No comment provided by engineer. */ "Enable disappearing messages by default." = "Включите исчезающие сообщения по умолчанию."; @@ -2044,11 +2292,11 @@ chat item action */ /* No comment provided by engineer. */ "Enable instant notifications?" = "Включить мгновенные уведомления?"; -/* No comment provided by engineer. */ -"Enable lock" = "Включить блокировку"; +/* alert title */ +"Enable link previews?" = "Включить картинки ссылок?"; /* No comment provided by engineer. */ -"Enable notifications" = "Включить уведомления"; +"Enable lock" = "Включить блокировку"; /* No comment provided by engineer. */ "Enable periodic notifications?" = "Включить периодические уведомления?"; @@ -2090,7 +2338,7 @@ chat item action */ "Encrypt local files" = "Шифровать локальные файлы"; /* No comment provided by engineer. */ -"Encrypt stored files & media" = "Шифруйте сохраненные файлы и медиа"; +"Encrypt stored files & media" = "Шифруйте сохранённые файлы и медиа"; /* No comment provided by engineer. */ "Encrypted database" = "База данных зашифрована"; @@ -2111,7 +2359,7 @@ chat item action */ "Encrypted message: keychain error" = "Зашифрованное сообщение: ошибка Keychain"; /* notification */ -"Encrypted message: no passphrase" = "Зашифрованное сообщение: пароль не сохранен"; +"Encrypted message: no passphrase" = "Зашифрованное сообщение: пароль не сохранён"; /* notification */ "Encrypted message: unexpected error" = "Зашифрованное сообщение: неожиданная ошибка"; @@ -2155,6 +2403,9 @@ chat item action */ /* call status */ "ended call %@" = "завершённый звонок %@"; +/* No comment provided by engineer. */ +"Enter channel name…" = "Введите имя канала…"; + /* No comment provided by engineer. */ "Enter correct passphrase." = "Введите правильный пароль."; @@ -2173,6 +2424,12 @@ chat item action */ /* No comment provided by engineer. */ "Enter password above to show!" = "Введите пароль выше, чтобы раскрыть!"; +/* No comment provided by engineer. */ +"Enter profile name..." = "Введите имя профиля..."; + +/* No comment provided by engineer. */ +"Enter relay name…" = "Введите имя релея…"; + /* No comment provided by engineer. */ "Enter server manually" = "Ввести сервер вручную"; @@ -2191,14 +2448,14 @@ chat item action */ /* No comment provided by engineer. */ "error" = "ошибка"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "Ошибка"; /* No comment provided by engineer. */ "Error aborting address change" = "Ошибка при прекращении изменения адреса"; /* alert title */ -"Error accepting conditions" = "Ошибка приема условий"; +"Error accepting conditions" = "Ошибка приёма условий"; /* No comment provided by engineer. */ "Error accepting contact request" = "Ошибка при принятии запроса на соединение"; @@ -2209,6 +2466,9 @@ chat item action */ /* No comment provided by engineer. */ "Error adding member(s)" = "Ошибка при добавлении членов группы"; +/* alert title */ +"Error adding relay" = "Ошибка добавления релея"; + /* alert title */ "Error adding server" = "Ошибка добавления сервера"; @@ -2245,6 +2505,9 @@ chat item action */ /* No comment provided by engineer. */ "Error creating address" = "Ошибка при создании адреса"; +/* alert title */ +"Error creating channel" = "Ошибка при создании канала"; + /* No comment provided by engineer. */ "Error creating group" = "Ошибка при создании группы"; @@ -2326,9 +2589,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Ошибка при открытии чата"; -/* No comment provided by engineer. */ -"Error opening group" = "Ошибка при открытии группы"; - /* alert title */ "Error receiving file" = "Ошибка при получении файла"; @@ -2353,6 +2613,9 @@ chat item action */ /* No comment provided by engineer. */ "Error resetting statistics" = "Ошибка сброса статистики"; +/* No comment provided by engineer. */ +"Error saving channel profile" = "Ошибка при сохранении профиля канала"; + /* alert title */ "Error saving chat list" = "Ошибка сохранения списка чатов"; @@ -2360,7 +2623,7 @@ chat item action */ "Error saving group profile" = "Ошибка при сохранении профиля группы"; /* No comment provided by engineer. */ -"Error saving ICE servers" = "Ошибка при сохранении ICE серверов"; +"Error saving ICE servers" = "Ошибка при сохранении ICE-серверов"; /* No comment provided by engineer. */ "Error saving passcode" = "Ошибка сохранения кода"; @@ -2395,6 +2658,9 @@ chat item action */ /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Ошибка настроек отчётов о доставке!"; +/* alert title */ +"Error sharing channel" = "Ошибка при публикации канала"; + /* No comment provided by engineer. */ "Error starting chat" = "Ошибка при запуске чата"; @@ -2437,12 +2703,16 @@ chat item action */ /* No comment provided by engineer. */ "Error: " = "Ошибка: "; +/* receive error chat item */ +"error: %@" = "ошибка: %@"; + /* alert message file error text snd error text */ "Error: %@" = "Ошибка: %@"; -/* server test error */ +/* relay test error +server test error */ "Error: %@." = "Ошибка: %@."; /* No comment provided by engineer. */ @@ -2490,6 +2760,9 @@ snd error text */ /* No comment provided by engineer. */ "Exporting database archive…" = "Архив чата экспортируется…"; +/* No comment provided by engineer. */ +"failed" = "ошибка"; + /* No comment provided by engineer. */ "Failed to remove passphrase" = "Ошибка удаления пароля"; @@ -2500,7 +2773,7 @@ snd error text */ "Faster deletion of groups." = "Ускорено удаление групп."; /* No comment provided by engineer. */ -"Faster joining and more reliable messages." = "Быстрое вступление и надежная доставка сообщений."; +"Faster joining and more reliable messages." = "Быстрое вступление и надёжная доставка сообщений."; /* No comment provided by engineer. */ "Faster sending messages." = "Ускорена отправка сообщений."; @@ -2521,7 +2794,7 @@ snd error text */ "File is blocked by server operator:\n%@." = "Файл заблокирован оператором сервера:\n%@."; /* file error text */ -"File not found - most likely file was deleted or cancelled." = "Файл не найден - скорее всего, файл был удален или отменен."; +"File not found - most likely file was deleted or cancelled." = "Файл не найден - скорее всего, файл был удалён или отменен."; /* file error text */ "File server error: %@" = "Ошибка сервера файлов: %@"; @@ -2557,7 +2830,7 @@ snd error text */ "Files and media are prohibited in this chat." = "Файлы и медиа запрещены в этом чате."; /* No comment provided by engineer. */ -"Files and media are prohibited." = "Файлы и медиа запрещены в этой группе."; +"Files and media are prohibited." = "Файлы и медиа запрещены."; /* No comment provided by engineer. */ "Files and media not allowed" = "Файлы и медиа не разрешены"; @@ -2565,6 +2838,9 @@ snd error text */ /* No comment provided by engineer. */ "Files and media prohibited!" = "Файлы и медиа запрещены!"; +/* No comment provided by engineer. */ +"Filter" = "Фильтр"; + /* No comment provided by engineer. */ "Filter unread and favorite chats." = "Фильтровать непрочитанные и избранные чаты."; @@ -2578,7 +2854,7 @@ snd error text */ "Finally, we have them! 🚀" = "Наконец-то, мы их добавили! 🚀"; /* No comment provided by engineer. */ -"Find chats faster" = "Быстро найти чаты"; +"Find chats faster" = "Быстрый поиск чатов"; /* No comment provided by engineer. */ "Fingerprint in destination server address does not match certificate: %@." = "Хэш в адресе сервера назначения не соответствует сертификату: %@."; @@ -2589,8 +2865,9 @@ snd error text */ /* No comment provided by engineer. */ "Fingerprint in server address does not match certificate: %@." = "Хэш в адресе сервера не соответствует сертификату: %@."; -/* server test error */ -"Fingerprint in server address does not match certificate." = "Возможно, хэш сертификата в адресе сервера неверный."; +/* relay test error +server test error */ +"Fingerprint in server address does not match certificate." = "Хэш в адресе сервера не соответствует сертификату."; /* No comment provided by engineer. */ "Fix" = "Починить"; @@ -2608,12 +2885,16 @@ snd error text */ "Fix not supported by contact" = "Починка не поддерживается контактом"; /* No comment provided by engineer. */ -"Fix not supported by group member" = "Починка не поддерживается членом группы."; +"Fix not supported by group member" = "Починка не поддерживается членом группы"; /* No comment provided by engineer. */ "For all moderators" = "Для всех модераторов"; -/* servers error */ +/* No comment provided by engineer. */ +"For anyone to reach you" = "Любой может связаться с Вами"; + +/* servers error +servers warning */ "For chat profile %@:" = "Для профиля чата %@:"; /* No comment provided by engineer. */ @@ -2697,9 +2978,15 @@ snd error text */ /* No comment provided by engineer. */ "Further reduced battery usage" = "Уменьшенное потребление батареи"; +/* relay test step */ +"Get link" = "Получить ссылку"; + /* No comment provided by engineer. */ "Get notified when mentioned." = "Уведомления, когда Вас упомянули."; +/* No comment provided by engineer. */ +"Get started" = "Начать"; + /* No comment provided by engineer. */ "GIFs and stickers" = "ГИФ файлы и стикеры"; @@ -2745,7 +3032,7 @@ snd error text */ /* No comment provided by engineer. */ "group is deleted" = "группа удалена"; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "Ссылка группы"; /* No comment provided by engineer. */ @@ -2767,7 +3054,7 @@ snd error text */ "Group profile is stored on members' devices, not on the servers." = "Профиль группы хранится на устройствах членов, а не на серверах."; /* snd group event chat item */ -"group profile updated" = "профиль группы обновлен"; +"group profile updated" = "профиль группы обновлён"; /* alert message */ "Group profile was changed. If you save it, the updated profile will be sent to group members." = "Профиль группы изменен. Если Вы сохраните его, новый профиль будет отправлен членам группы."; @@ -2788,7 +3075,7 @@ snd error text */ "Help" = "Помощь"; /* No comment provided by engineer. */ -"Help admins moderating their groups." = "Помогайте администраторам модерировать их группы."; +"Help admins moderating their groups." = "Помогайте админам модерировать их группы."; /* No comment provided by engineer. */ "Hidden" = "Скрытое"; @@ -2800,7 +3087,7 @@ snd error text */ "Hidden profile password" = "Пароль скрытого профиля"; /* chat item action */ -"Hide" = "Спрятать"; +"Hide" = "Скрыть"; /* No comment provided by engineer. */ "Hide app screen in the recent apps." = "Скрыть экран приложения."; @@ -2817,6 +3104,9 @@ snd error text */ /* No comment provided by engineer. */ "History is not sent to new members." = "История не отправляется новым членам."; +/* No comment provided by engineer. */ +"History is not sent to new subscribers." = "История не отправляется новым подписчикам."; + /* time unit */ "hours" = "часов"; @@ -2836,7 +3126,7 @@ snd error text */ "How to" = "Инфо"; /* No comment provided by engineer. */ -"How to use it" = "Про адрес"; +"How to use it" = "Как использовать"; /* No comment provided by engineer. */ "How to use your servers" = "Как использовать серверы"; @@ -2845,7 +3135,7 @@ snd error text */ "Hungarian interface" = "Венгерский интерфейс"; /* No comment provided by engineer. */ -"ICE servers (one per line)" = "ICE серверы (один на строке)"; +"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-код во время видеозвонка или поделитесь ссылкой."; @@ -2856,6 +3146,9 @@ snd error text */ /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "Если Вы введёте код самоуничтожения при открытии приложения:"; +/* down migration warning */ +"If you joined or created channels, they will stop working permanently." = "Если Вы присоединились к каналам или создали их, они перестанут работать навсегда."; + /* 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)." = "Если сейчас Вам нужно использовать чат, нажмите **Отложить** внизу (Вы сможете мигрировать данные чата при следующем запуске приложения)."; @@ -2869,10 +3162,10 @@ snd error text */ "Image will be received when your contact is online, please wait or check later!" = "Изображение будет принято, когда Ваш контакт будет в сети, подождите или проверьте позже!"; /* No comment provided by engineer. */ -"Immediately" = "Сразу"; +"Images" = "Изображения"; /* No comment provided by engineer. */ -"Immune to spam" = "Защищен от спама"; +"Immediately" = "Сразу"; /* No comment provided by engineer. */ "Import" = "Импортировать"; @@ -2932,7 +3225,7 @@ snd error text */ "Incognito mode" = "Режим Инкогнито"; /* No comment provided by engineer. */ -"Incognito mode protects your privacy by using a new random profile for each contact." = "Режим Инкогнито защищает Вашу конфиденциальность — для каждого контакта создается новый случайный профиль."; +"Incognito mode protects your privacy by using a new random profile for each contact." = "Режим Инкогнито защищает Вашу конфиденциальность - для каждого контакта создаётся новый случайный профиль."; /* chat list item description */ "incognito via contact address link" = "инкогнито через ссылку-контакт"; @@ -2974,7 +3267,7 @@ snd error text */ "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)"; +"Install SimpleX Chat for terminal" = "SimpleX Chat для терминала"; /* No comment provided by engineer. */ "Instant" = "Мгновенно"; @@ -3009,7 +3302,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "ошибка данных чата"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "Ошибка в ссылке контакта"; /* invalid chat item */ @@ -3024,11 +3317,17 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Ошибка подтверждения миграции"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Неверное имя!"; /* No comment provided by engineer. */ -"Invalid QR code" = "Неверный QR код"; +"Invalid QR code" = "Ошибка QR-кода"; + +/* alert title */ +"Invalid relay address!" = "Неверный адрес релея!"; + +/* alert title */ +"Invalid relay name!" = "Неверное имя релея!"; /* No comment provided by engineer. */ "Invalid response" = "Ошибка ответа"; @@ -3052,7 +3351,13 @@ snd error text */ "Invite friends" = "Пригласить друзей"; /* No comment provided by engineer. */ -"Invite members" = "Пригласить членов группы"; +"Invite member" = "Пригласить члена группы"; + +/* No comment provided by engineer. */ +"Invite members" = "Пригласить в группу"; + +/* No comment provided by engineer. */ +"Invite someone privately" = "Пригласите конфиденциально"; /* No comment provided by engineer. */ "Invite to chat" = "Пригласить в разговор"; @@ -3076,19 +3381,19 @@ snd error text */ "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 после запуска чата или изменения пароля - это позволит получать мгновенные уведомления."; +"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. */ -"IP address" = "IP адрес"; +"IP address" = "IP-адрес"; /* No comment provided by engineer. */ "Irreversible message deletion" = "Окончательное удаление сообщений"; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this chat." = "Необратимое удаление сообщений запрещено в этом чате."; +"Irreversible message deletion is prohibited in this chat." = "Необратимое удаление сообщений запрещено."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited." = "Необратимое удаление сообщений запрещено в этой группе."; +"Irreversible message deletion is prohibited." = "Необратимое удаление сообщений запрещено."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя."; @@ -3100,7 +3405,7 @@ snd error text */ "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 protects your IP address and connections." = "Защищает ваш IP адрес и соединения."; +"It protects your IP address and connections." = "Защищает ваш IP-адрес и соединения."; /* 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 (%@)." = "Возможно, Вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@)."; @@ -3118,7 +3423,10 @@ snd error text */ "Join" = "Вступить"; /* No comment provided by engineer. */ -"Join as %@" = "вступить как %@"; +"Join as %@" = "Вступить как %s"; + +/* No comment provided by engineer. */ +"Join channel" = "Вступить в канал"; /* new chat sheet title */ "Join group" = "Вступить в группу"; @@ -3168,6 +3476,12 @@ snd error text */ /* swipe action */ "Leave" = "Выйти"; +/* No comment provided by engineer. */ +"Leave channel" = "Покинуть канал"; + +/* No comment provided by engineer. */ +"Leave channel?" = "Выйти из канала?"; + /* No comment provided by engineer. */ "Leave chat" = "Покинуть разговор"; @@ -3186,6 +3500,9 @@ snd error text */ /* No comment provided by engineer. */ "Less traffic on mobile networks." = "Меньше трафик в мобильных сетях."; +/* No comment provided by engineer. */ +"Let someone connect to you" = "Дайте собеседнику Вашу ссылку"; + /* email subject */ "Let's talk in SimpleX Chat" = "Давайте поговорим в SimpleX Chat"; @@ -3195,15 +3512,24 @@ snd error text */ /* No comment provided by engineer. */ "Limitations" = "Ограничения"; +/* No comment provided by engineer. */ +"link" = "ссылка"; + /* No comment provided by engineer. */ "Link mobile and desktop apps! 🔗" = "Свяжите мобильное и настольное приложения! 🔗"; +/* owner verification */ +"Link signature verified." = "Подпись ссылки проверена."; + /* No comment provided by engineer. */ "Linked desktop options" = "Опции связанных компьютеров"; /* No comment provided by engineer. */ "Linked desktops" = "Связанные компьютеры"; +/* No comment provided by engineer. */ +"Links" = "Ссылки"; + /* swipe action */ "List" = "Список"; @@ -3244,10 +3570,10 @@ snd error text */ "Make profile private!" = "Сделайте профиль скрытым!"; /* No comment provided by engineer. */ -"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Пожалуйста, проверьте, что адреса WebRTC ICE серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется."; +"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Пожалуйста, проверьте, что адреса WebRTC ICE-серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется."; /* No comment provided by engineer. */ -"Mark deleted for everyone" = "Пометить как удаленное для всех"; +"Mark deleted for everyone" = "Пометить как удалённое для всех"; /* No comment provided by engineer. */ "Mark read" = "Прочитано"; @@ -3289,14 +3615,17 @@ snd error text */ "member connected" = "соединен(а)"; /* No comment provided by engineer. */ -"member has old version" = "член имеет старую версию"; +"member has old version" = "член группы имеет старую версию"; /* item status text */ -"Member inactive" = "Член неактивен"; +"Member inactive" = "Член группы неактивен"; /* No comment provided by engineer. */ "Member is deleted - can't accept request" = "Член группы удалён - невозможно принять запрос"; +/* alert message */ +"Member messages will be deleted - this cannot be undone!" = "Сообщения члена группы будут удалены - это нельзя отменить!"; + /* chat feature */ "Member reports" = "Сообщения о нарушениях"; @@ -3310,17 +3639,20 @@ snd error text */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Роль члена будет изменена на \"%@\". Будет отправлено новое приглашение."; /* alert message */ -"Member will be removed from chat - this cannot be undone!" = "Член будет удален из разговора - это действие нельзя отменить!"; +"Member will be removed from chat - this cannot be undone!" = "Член будет удалён из разговора - это действие нельзя отменить!"; /* alert message */ -"Member will be removed from group - this cannot be undone!" = "Член группы будет удален - это действие нельзя отменить!"; +"Member will be removed from group - this cannot be undone!" = "Член группы будет удалён - это действие нельзя отменить!"; /* alert message */ -"Member will join the group, accept member?" = "Участник хочет присоединиться к группе. Принять?"; +"Member will join the group, accept member?" = "Член группы хочет присоединиться. Принять?"; /* No comment provided by engineer. */ "Members can add message reactions." = "Члены могут добавлять реакции на сообщения."; +/* No comment provided by engineer. */ +"Members can chat with admins." = "Члены группы могут общаться с админами."; + /* No comment provided by engineer. */ "Members can irreversibly delete sent messages. (24 hours)" = "Члены могут необратимо удалять отправленные сообщения. (24 часа)"; @@ -3328,7 +3660,7 @@ snd error text */ "Members can report messsages to moderators." = "Члены группы могут пожаловаться модераторам."; /* No comment provided by engineer. */ -"Members can send direct messages." = "Члены могут посылать прямые сообщения."; +"Members can send direct messages." = "Члены могут посылать личные сообщения."; /* No comment provided by engineer. */ "Members can send disappearing messages." = "Члены могут посылать исчезающие сообщения."; @@ -3337,13 +3669,13 @@ snd error text */ "Members can send files and media." = "Члены могут слать файлы и медиа."; /* No comment provided by engineer. */ -"Members can send SimpleX links." = "Члены могут отправлять ссылки SimpleX."; +"Members can send SimpleX links." = "Члены группы могут отправлять ссылки SimpleX."; /* No comment provided by engineer. */ -"Members can send voice messages." = "Члены могут отправлять голосовые сообщения."; +"Members can send voice messages." = "Члены группы могут отправлять голосовые сообщения."; /* No comment provided by engineer. */ -"Mention members 👋" = "Упоминайте участников 👋"; +"Mention members 👋" = "Упоминайте членов группы 👋"; /* No comment provided by engineer. */ "Menus" = "Меню"; @@ -3355,7 +3687,7 @@ snd error text */ "Message delivery error" = "Ошибка доставки сообщения"; /* No comment provided by engineer. */ -"Message delivery receipts!" = "Отчеты о доставке сообщений!"; +"Message delivery receipts!" = "Отчёты о доставке сообщений!"; /* item status text */ "Message delivery warning" = "Предупреждение доставки сообщения"; @@ -3363,6 +3695,9 @@ snd error text */ /* No comment provided by engineer. */ "Message draft" = "Черновик сообщения"; +/* No comment provided by engineer. */ +"Message error" = "Ошибка сообщения"; + /* item status text */ "Message forwarded" = "Сообщение переслано"; @@ -3382,13 +3717,13 @@ snd error text */ "Message reactions are prohibited in this chat." = "Реакции на сообщения в этом чате запрещены."; /* No comment provided by engineer. */ -"Message reactions are prohibited." = "Реакции на сообщения запрещены в этой группе."; +"Message reactions are prohibited." = "Реакции на сообщения запрещены."; /* notification */ "message received" = "получено сообщение"; /* No comment provided by engineer. */ -"Message reception" = "Прием сообщений"; +"Message reception" = "Приём сообщений"; /* No comment provided by engineer. */ "Message servers" = "Серверы сообщений"; @@ -3418,11 +3753,17 @@ snd error text */ "Messages & files" = "Сообщения"; /* No comment provided by engineer. */ -"Messages are protected by **end-to-end encryption**." = "Сообщения защищены **end-to-end шифрованием**."; +"Messages are protected by **end-to-end encryption**." = "Сообщения защищены **сквозным шифрованием**."; /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Сообщения от %@ будут показаны!"; +/* No comment provided by engineer. */ +"Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages." = "Сообщения в этом канале **не защищены сквозным шифрованием**. Чат-релеи могут видеть эти сообщения."; + +/* E2EE info chat item */ +"Messages in this channel are not end-to-end encrypted. Chat relays can see these messages." = "Сообщения в этом канале не защищены сквозным шифрованием. Чат-релеи могут видеть эти сообщения."; + /* alert message */ "Messages in this chat will never be deleted." = "Сообщения в этом чате никогда не будут удалены."; @@ -3436,17 +3777,17 @@ snd error text */ "Messages were deleted after you selected them." = "Сообщения были удалены после того, как вы их выбрали."; /* No comment provided by engineer. */ -"Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Сообщения, файлы и звонки защищены **end-to-end шифрованием** с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома."; +"Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Сообщения, файлы и звонки защищены **сквозным шифрованием** с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома."; /* No comment provided by engineer. */ -"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Сообщения, файлы и звонки защищены **квантово-устойчивым end-to-end шифрованием** с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома."; +"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Сообщения, файлы и звонки защищены **квантово-устойчивым сквозным шифрованием** с идеальной прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома."; + +/* No comment provided by engineer. */ +"Migrate" = "Мигрировать"; /* No comment provided by engineer. */ "Migrate device" = "Мигрировать устройство"; -/* No comment provided by engineer. */ -"Migrate from another device" = "Миграция с другого устройства"; - /* No comment provided by engineer. */ "Migrate here" = "Мигрировать сюда"; @@ -3454,7 +3795,7 @@ snd error text */ "Migrate to another device" = "Мигрировать на другое устройство"; /* No comment provided by engineer. */ -"Migrate to another device via QR code." = "Мигрируйте на другое устройство через QR код."; +"Migrate to another device via QR code." = "Мигрируйте на другое устройство через QR-код."; /* No comment provided by engineer. */ "Migrating" = "Миграция"; @@ -3511,10 +3852,10 @@ snd error text */ "More improvements are coming soon!" = "Дополнительные улучшения скоро!"; /* No comment provided by engineer. */ -"More reliable network connection." = "Более надежное соединение с сетью."; +"More reliable network connection." = "Более надёжное соединение с сетью."; /* No comment provided by engineer. */ -"More reliable notifications" = "Более надежные уведомления"; +"More reliable notifications" = "Более надёжные уведомления"; /* item status description */ "Most likely this connection is deleted." = "Скорее всего, соединение удалено."; @@ -3537,12 +3878,18 @@ snd error text */ /* No comment provided by engineer. */ "Network & servers" = "Сеть и серверы"; +/* No comment provided by engineer. */ +"Network commitments" = "Обязательства сети"; + /* No comment provided by engineer. */ "Network connection" = "Интернет-соединение"; /* No comment provided by engineer. */ "Network decentralization" = "Децентрализация сети"; +/* conn error description */ +"Network error" = "Ошибка сети"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "Ошибка сети - сообщение не было отправлено после многократных попыток."; @@ -3552,6 +3899,9 @@ snd error text */ /* No comment provided by engineer. */ "Network operator" = "Оператор сети"; +/* No comment provided by engineer. */ +"Network routers cannot know\nwho talks to whom" = "Серверы сети не могут знать,\nкто с кем общается"; + /* No comment provided by engineer. */ "Network settings" = "Настройки сети"; @@ -3561,15 +3911,24 @@ snd error text */ /* delete after time */ "never" = "никогда"; +/* No comment provided by engineer. */ +"new" = "новый"; + /* token status text */ "New" = "Новый"; +/* No comment provided by engineer. */ +"New 1-time link" = "Новая одноразовая ссылка"; + /* No comment provided by engineer. */ "New chat" = "Новый чат"; /* No comment provided by engineer. */ "New chat experience 🎉" = "Новый интерфейс 🎉"; +/* No comment provided by engineer. */ +"New chat relay" = "Новый чат-релей"; + /* notification */ "New contact request" = "Новый запрос на соединение"; @@ -3598,7 +3957,7 @@ snd error text */ "New member role" = "Роль члена группы"; /* rcv group event chat item */ -"New member wants to join the group." = "Новый участник хочет присоединиться к группе."; +"New member wants to join the group." = "Новый член группы хочет присоединиться."; /* notification */ "new message" = "новое сообщение"; @@ -3616,10 +3975,10 @@ snd error text */ "New server" = "Новый сервер"; /* No comment provided by engineer. */ -"New SOCKS credentials will be used every time you start the app." = "Новые учетные данные SOCKS будут использоваться при каждом запуске приложения."; +"New SOCKS credentials will be used every time you start the app." = "Новые учётные данные SOCKS будут использоваться при каждом запуске приложения."; /* No comment provided by engineer. */ -"New SOCKS credentials will be used for each server." = "Новые учетные данные SOCKS будут использоваться для каждого сервера."; +"New SOCKS credentials will be used for each server." = "Новые учётные данные SOCKS будут использоваться для каждого сервера."; /* pref value */ "no" = "нет"; @@ -3627,9 +3986,21 @@ snd error text */ /* No comment provided by engineer. */ "No" = "Нет"; +/* No comment provided by engineer. */ +"No account. No phone. No email. No ID.\nThe most secure encryption." = "Без аккаунта. Без номера. Без email. Без ID.\nСамое безопасное шифрование."; + +/* No comment provided by engineer. */ +"No active relays" = "Нет активных релеев"; + /* Authentication unavailable */ "No app password" = "Нет кода доступа"; +/* No comment provided by engineer. */ +"No chat relays" = "Нет чат-релеев"; + +/* servers warning */ +"No chat relays enabled." = "Чат-релеи не включены."; + /* No comment provided by engineer. */ "No chats" = "Нет чатов"; @@ -3706,10 +4077,10 @@ snd error text */ "No servers for private message routing." = "Нет серверов для доставки сообщений."; /* servers error */ -"No servers to receive files." = "Нет серверов для приема файлов."; +"No servers to receive files." = "Нет серверов для приёма файлов."; /* servers error */ -"No servers to receive messages." = "Нет серверов для приема сообщений."; +"No servers to receive messages." = "Нет серверов для приёма сообщений."; /* servers error */ "No servers to send files." = "Нет серверов для отправки файлов."; @@ -3727,7 +4098,16 @@ snd error text */ "No unread chats" = "Нет непрочитанных чатов"; /* No comment provided by engineer. */ -"No user identifiers." = "Без идентификаторов пользователей."; +"Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "Никто не отслеживал ваши разговоры. Никто не составлял карту ваших перемещений. Конфиденциальность не была функцией - это был образ жизни."; + +/* No comment provided by engineer. */ +"Non-profit governance" = "Некоммерческое управление"; + +/* No comment provided by engineer. */ +"Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "Не более надёжный замок на чужой двери. Не более вежливый хозяин, который уважает вашу частную жизнь, но всё равно ведёт учёт всех посетителей. Вы не гость. Вы у себя дома. Ни один король не войдёт в ваш дом - вы суверенны."; + +/* alert title */ +"Not all relays connected" = "Не все релеи подключены"; /* No comment provided by engineer. */ "Not compatible!" = "Несовместимая версия!"; @@ -3772,7 +4152,7 @@ time to disappear */ "off" = "нет"; /* blur media */ -"Off" = "Выключено"; +"Off" = "Нет"; /* feature offered item */ "offered %@" = "предложил(a) %@"; @@ -3785,7 +4165,7 @@ alert button new chat action */ "Ok" = "Ок"; -/* No comment provided by engineer. */ +/* alert button */ "OK" = "OK"; /* No comment provided by engineer. */ @@ -3794,9 +4174,15 @@ new chat action */ /* group pref value */ "on" = "да"; +/* No comment provided by engineer. */ +"On your phone, not on servers." = "На Вашем телефоне, не на серверах."; + /* No comment provided by engineer. */ "One-time invitation link" = "Одноразовая ссылка"; +/* chat link info line */ +"One-time link" = "Одноразовая ссылка"; + /* No comment provided by engineer. */ "Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Подключаться только к **onion** хостам.\nТребуется совместимый VPN."; @@ -3806,6 +4192,9 @@ new chat action */ /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion хосты не используются."; +/* No comment provided by engineer. */ +"Only channel owners can change channel preferences." = "Изменить настройки канала могут только владельцы канала."; + /* No comment provided by engineer. */ "Only chat owners can change preferences." = "Только владельцы разговора могут поменять предпочтения."; @@ -3866,12 +4255,16 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Только Ваш контакт может отправлять голосовые сообщения."; -/* alert action */ +/* alert action +alert button */ "Open" = "Открыть"; /* No comment provided by engineer. */ "Open changes" = "Открыть изменения"; +/* new chat action */ +"Open channel" = "Открыть канал"; + /* new chat action */ "Open chat" = "Открыть чат"; @@ -3884,6 +4277,9 @@ new chat action */ /* No comment provided by engineer. */ "Open conditions" = "Открыть условия"; +/* alert title */ +"Open external link?" = "Открыть внешнюю ссылку?"; + /* alert action */ "Open full link" = "Открыть полную ссылку"; @@ -3896,6 +4292,9 @@ new chat action */ /* authentication reason */ "Open migration to another device" = "Открытие миграции на другое устройство"; +/* new chat action */ +"Open new channel" = "Открыть новый канал"; + /* new chat action */ "Open new chat" = "Открыть новый чат"; @@ -3926,6 +4325,9 @@ new chat action */ /* alert title */ "Operator server" = "Сервер оператора"; +/* No comment provided by engineer. */ +"Operators commit to:\n- Be independent\n- Minimize metadata usage\n- Run verified open-source code" = "Операторы обязуются:\n- Быть независимыми\n- Минимизировать использование метаданных\n- Использовать проверенный и открытый исходный код"; + /* No comment provided by engineer. */ "Or import archive file" = "Или импортировать файл архива"; @@ -3933,17 +4335,23 @@ new chat action */ "Or paste archive link" = "Или вставьте ссылку архива"; /* No comment provided by engineer. */ -"Or scan QR code" = "Или отсканируйте QR код"; +"Or scan QR code" = "Или отсканируйте QR-код"; /* No comment provided by engineer. */ "Or securely share this file link" = "Или передайте эту ссылку"; +/* No comment provided by engineer. */ +"Or show QR in person or via video call." = "Или покажите QR лично или через видеозвонок."; + /* No comment provided by engineer. */ "Or show this code" = "Или покажите этот код"; /* No comment provided by engineer. */ "Or to share privately" = "Или поделиться конфиденциально"; +/* No comment provided by engineer. */ +"Or use this QR - print or show online." = "Или используйте этот QR - распечатайте или покажите онлайн."; + /* No comment provided by engineer. */ "Organize chats into lists" = "Организуйте чаты в списки"; @@ -3962,9 +4370,18 @@ new chat action */ /* member role */ "owner" = "владелец"; +/* No comment provided by engineer. */ +"Owner" = "Владелец"; + /* feature role */ "owners" = "владельцы"; +/* No comment provided by engineer. */ +"Owners" = "Владельцы"; + +/* No comment provided by engineer. */ +"Ownership: you can run your own relays." = "Владение: Вы можете запустить свои собственные релеи."; + /* No comment provided by engineer. */ "Passcode" = "Код доступа"; @@ -3992,6 +4409,9 @@ new chat action */ /* No comment provided by engineer. */ "Paste image" = "Вставить изображение"; +/* No comment provided by engineer. */ +"Paste link / Scan" = "Вставить ссылку / Сканировать"; + /* No comment provided by engineer. */ "Paste link to connect!" = "Вставьте ссылку, чтобы соединиться!"; @@ -4041,10 +4461,10 @@ new chat action */ "Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Пожалуйста, проверьте, что мобильный и компьютер находятся в одной и той же локальной сети, и что брандмауэр компьютера разрешает подключение.\nПожалуйста, поделитесь любыми другими ошибками с разработчиками."; /* No comment provided by engineer. */ -"Please check that you used the correct link or ask your contact to send you another one." = "Пожалуйста, проверьте, что Вы использовали правильную ссылку или попросите, чтобы Ваш контакт отправил Вам другую ссылку."; +"Please check that you used the correct link or ask your contact to send you another one." = "Пожалуйста, проверьте, что Вы использовали правильную ссылку, или попросите Ваш контакт отправить Вам новую."; /* alert message */ -"Please check your network connection with %@ and try again." = "Пожалуйста, проверьте Ваше соединение с %@ и попробуйте еще раз."; +"Please check your network connection with %@ and try again." = "Пожалуйста, проверьте Ваше соединение с %@ и попробуйте ещё раз."; /* No comment provided by engineer. */ "Please check yours and your contact preferences." = "Проверьте предпочтения Вашего контакта."; @@ -4068,16 +4488,16 @@ new chat action */ "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." = "Пожалуйста, сообщите об этой ошибке девелоперам."; +"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." = "Пожалуйста, надежно сохраните пароль, Вы НЕ сможете открыть чат, если потеряете его."; +"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." = "Пожалуйста, надежно сохраните пароль, Вы НЕ сможете его поменять, если потеряете."; +"Please store passphrase securely, you will NOT be able to change it if you lose it." = "Пожалуйста, надёжно сохраните пароль, Вы НЕ сможете его поменять, если потеряете."; /* token info */ "Please try to disable and re-enable notfications." = "Попробуйте выключить и снова включить уведомления."; @@ -4100,6 +4520,12 @@ new chat action */ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Сохранить последний черновик, вместе с вложениями."; +/* No comment provided by engineer. */ +"Preset relay address" = "Адрес релея по умолчанию"; + +/* No comment provided by engineer. */ +"Preset relay name" = "Имя релея по умолчанию"; + /* No comment provided by engineer. */ "Preset server address" = "Адрес сервера по умолчанию"; @@ -4122,13 +4548,13 @@ new chat action */ "Privacy policy and conditions of use." = "Политика конфиденциальности и условия использования."; /* No comment provided by engineer. */ -"Privacy redefined" = "Более конфиденциальный"; +"Privacy: for owners and subscribers." = "Конфиденциальность: для владельцев и подписчиков."; /* No comment provided by engineer. */ -"Private chats, groups and your contacts are not accessible to server operators." = "Частные разговоры, группы и Ваши контакты недоступны для операторов серверов."; +"Private and secure messaging." = "Конфиденциальный и безопасный обмен сообщениями."; /* No comment provided by engineer. */ -"Private filenames" = "Защищенные имена файлов"; +"Private filenames" = "Защищённые имена файлов"; /* No comment provided by engineer. */ "Private media file names." = "Конфиденциальные названия медиафайлов."; @@ -4151,6 +4577,9 @@ new chat action */ /* alert title */ "Private routing timeout" = "Таймаут конфиденциальной доставки"; +/* alert action */ +"Proceed" = "Продолжить"; + /* No comment provided by engineer. */ "Profile and server connections" = "Профиль и соединения на сервере"; @@ -4167,11 +4596,14 @@ new chat action */ "Profile theme" = "Тема профиля"; /* alert message */ -"Profile update will be sent to your contacts." = "Обновлённый профиль будет отправлен Вашим контактам."; +"Profile update will be sent to your SimpleX contacts." = "Обновление профиля будет отправлено Вашим SimpleX контактам."; /* No comment provided by engineer. */ "Prohibit audio/video calls." = "Запретить аудио/видео звонки."; +/* No comment provided by engineer. */ +"Prohibit chats with admins." = "Запретить чаты с админами."; + /* No comment provided by engineer. */ "Prohibit irreversible message deletion." = "Запретить необратимое удаление сообщений."; @@ -4185,31 +4617,34 @@ new chat action */ "Prohibit reporting messages to moderators." = "Запретить жаловаться модераторам группы."; /* No comment provided by engineer. */ -"Prohibit sending direct messages to members." = "Запретить посылать прямые сообщения членам группы."; +"Prohibit sending direct messages to members." = "Запретить посылать личные сообщения членам группы."; /* No comment provided by engineer. */ -"Prohibit sending disappearing messages." = "Запретить посылать исчезающие сообщения."; +"Prohibit sending direct messages to subscribers." = "Запретить отправку личных сообщений подписчикам."; /* No comment provided by engineer. */ -"Prohibit sending files and media." = "Запретить слать файлы и медиа."; +"Prohibit sending disappearing messages." = "Запретить отправлять исчезающие сообщения."; + +/* No comment provided by engineer. */ +"Prohibit sending files and media." = "Запретить отправлять файлы и медиа."; /* No comment provided by engineer. */ "Prohibit sending SimpleX links." = "Запретить отправку ссылок SimpleX."; /* No comment provided by engineer. */ -"Prohibit sending voice messages." = "Запретить отправлять голосовые сообщений."; +"Prohibit sending voice messages." = "Запретить отправлять голосовые сообщения."; /* No comment provided by engineer. */ "Protect app screen" = "Защитить экран приложения"; /* No comment provided by engineer. */ -"Protect IP address" = "Защитить IP адрес"; +"Protect IP address" = "Защитить IP-адрес"; /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Защитите Ваши профили чата паролем!"; /* No comment provided by engineer. */ -"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами.\nВключите в настройках *Сети и серверов*."; +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Защитите ваш IP-адрес от серверов сообщений, выбранных Вашими контактами.\nВключите в настройках *Сети и серверов*."; /* No comment provided by engineer. */ "Protocol background timeout" = "Фоновый таймаут протокола"; @@ -4229,6 +4664,9 @@ new chat action */ /* No comment provided by engineer. */ "Proxy requires password" = "Прокси требует пароль"; +/* No comment provided by engineer. */ +"Public channels - speak freely 🚀" = "Публичные каналы - говорите свободно 🚀"; + /* No comment provided by engineer. */ "Push notifications" = "Доставка уведомлений"; @@ -4257,22 +4695,16 @@ new chat action */ "Read more" = "Узнать больше"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Дополнительная информация в [Руководстве пользователя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in our GitHub repository." = "Узнайте больше из нашего GitHub репозитория."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; - -/* 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)."; +"Read more in User Guide." = "Узнать больше в Руководстве пользователя."; /* No comment provided by engineer. */ "Receipts are disabled" = "Отчёты о доставке выключены"; /* No comment provided by engineer. */ -"Receive errors" = "Ошибки приема"; +"Receive errors" = "Ошибки приёма"; /* No comment provided by engineer. */ "received answer…" = "получен ответ…"; @@ -4367,7 +4799,7 @@ swipe action */ "Reject contact request" = "Отклонить запрос"; /* alert title */ -"Reject member?" = "Отклонить участника?"; +"Reject member?" = "Отклонить члена группы?"; /* No comment provided by engineer. */ "rejected" = "отклонён"; @@ -4375,15 +4807,42 @@ swipe action */ /* call status */ "rejected call" = "отклонённый звонок"; -/* No comment provided by engineer. */ -"Relay server is only used if necessary. Another party can observe your IP address." = "Relay сервер используется только при необходимости. Другая сторона может видеть Ваш IP адрес."; +/* member role */ +"relay" = "релей"; /* No comment provided by engineer. */ -"Relay server protects your IP address, but it can observe the duration of the call." = "Relay сервер защищает Ваш IP адрес, но может отслеживать продолжительность звонка."; +"Relay" = "Релей"; + +/* alert title */ +"Relay address" = "Адрес релея"; + +/* alert title */ +"Relay connection failed" = "Ошибка подключения релея"; + +/* No comment provided by engineer. */ +"Relay link" = "Ссылка релея"; + +/* alert message */ +"Relay results:" = "Результаты релея:"; + +/* 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. */ +"Relay test failed!" = "Тест релея не пройден!"; + +/* No comment provided by engineer. */ +"Reliability: many relays per channel." = "Надёжность: несколько релеев на каждый канал."; /* alert action */ "Remove" = "Удалить"; +/* alert action */ +"Remove and delete messages" = "Удалить вместе с сообщениями"; + /* No comment provided by engineer. */ "Remove archive?" = "Удалить архив?"; @@ -4402,17 +4861,29 @@ swipe action */ /* No comment provided by engineer. */ "Remove passphrase from keychain?" = "Удалить пароль из Keychain?"; +/* No comment provided by engineer. */ +"Remove subscriber" = "Удалить подписчика"; + +/* alert title */ +"Remove subscriber?" = "Удалить подписчика?"; + /* No comment provided by engineer. */ "removed" = "удален(а)"; +/* receive error chat item */ +"removed (%d attempts)" = "удалено (%d попыток)"; + /* rcv group event chat item */ "removed %@" = "удалил(а) %@"; +/* No comment provided by engineer. */ +"removed by operator" = "удалено оператором"; + /* profile update event chat item */ "removed contact address" = "удалён адрес контакта"; /* No comment provided by engineer. */ -"removed from group" = "удален из группы"; +"removed from group" = "удалён из группы"; /* profile update event chat item */ "removed profile picture" = "удалена картинка профиля"; @@ -4520,10 +4991,10 @@ swipe action */ "Reset to user theme" = "Сбросить на тему пользователя"; /* No comment provided by engineer. */ -"Restart the app to create a new chat profile" = "Перезапустите приложение, чтобы создать новый профиль."; +"Restart the app to create a new chat profile" = "Перезапустите приложение, чтобы создать новый профиль"; /* No comment provided by engineer. */ -"Restart the app to use imported chat database" = "Перезапустите приложение, чтобы использовать импортированные данные чата."; +"Restart the app to use imported chat database" = "Перезапустите приложение, чтобы использовать импортированные данные чата"; /* No comment provided by engineer. */ "Restore" = "Восстановить"; @@ -4553,7 +5024,7 @@ swipe action */ "Review group members" = "Одобрять членов группы"; /* admission stage */ -"Review members" = "Одобрять членов"; +"Review members" = "Одобрять членов группы"; /* admission stage description */ "Review members before admitting (\"knocking\")." = "Вручную одобрять членов для вступления в группу."; @@ -4576,6 +5047,9 @@ swipe action */ /* No comment provided by engineer. */ "Run chat" = "Запустить chat"; +/* No comment provided by engineer. */ +"Safe web links" = "Безопасные веб-ссылки"; + /* No comment provided by engineer. */ "Safely receive files" = "Получайте файлы безопасно"; @@ -4592,6 +5066,9 @@ chat item action */ /* alert button */ "Save (and notify members)" = "Сохранить (и уведомить членов)"; +/* alert button */ +"Save (and notify subscribers)" = "Сохранить (и уведомить подписчиков)"; + /* alert title */ "Save admission settings?" = "Сохранить настройки вступления?"; @@ -4601,12 +5078,21 @@ chat item action */ /* No comment provided by engineer. */ "Save and notify group members" = "Сохранить и уведомить членов группы"; +/* No comment provided by engineer. */ +"Save and notify subscribers" = "Сохранить и уведомить подписчиков"; + /* No comment provided by engineer. */ "Save and reconnect" = "Сохранить и переподключиться"; /* No comment provided by engineer. */ "Save and update group profile" = "Сохранить сообщение и обновить группу"; +/* No comment provided by engineer. */ +"Save channel profile" = "Сохранить профиль канала"; + +/* alert title */ +"Save channel profile?" = "Сохранить профиль канала?"; + /* No comment provided by engineer. */ "Save group profile" = "Сохранить профиль группы"; @@ -4653,10 +5139,10 @@ chat item action */ "saved from %@" = "сохранено из %@"; /* message info title */ -"Saved message" = "Сохраненное сообщение"; +"Saved message" = "Сохранённое сообщение"; /* No comment provided by engineer. */ -"Saved WebRTC ICE servers will be removed" = "Сохраненные WebRTC ICE серверы будут удалены"; +"Saved WebRTC ICE servers will be removed" = "Сохранённые WebRTC ICE-серверы будут удалены"; /* No comment provided by engineer. */ "Saving %lld messages" = "Сохранение %lld сообщений"; @@ -4671,16 +5157,16 @@ chat item action */ "Scan code" = "Сканировать код"; /* No comment provided by engineer. */ -"Scan QR code" = "Сканировать QR код"; +"Scan QR code" = "Сканировать QR-код"; /* No comment provided by engineer. */ -"Scan QR code from desktop" = "Сканировать QR код с компьютера"; +"Scan QR code from desktop" = "Сканировать QR-код с компьютера"; /* No comment provided by engineer. */ "Scan security code from your contact's app." = "Сканируйте код безопасности из приложения контакта."; /* No comment provided by engineer. */ -"Scan server QR code" = "Сканировать QR код сервера"; +"Scan server QR code" = "Сканировать QR-код сервера"; /* No comment provided by engineer. */ "search" = "поиск"; @@ -4692,7 +5178,22 @@ chat item action */ "Search bar accepts invitation links." = "Поле поиска поддерживает ссылки-приглашения."; /* No comment provided by engineer. */ -"Search or paste SimpleX link" = "Искать или вставьте ссылку SimpleX"; +"Search files" = "Поиск файлов"; + +/* No comment provided by engineer. */ +"Search images" = "Поиск изображений"; + +/* No comment provided by engineer. */ +"Search links" = "Поиск ссылок"; + +/* No comment provided by engineer. */ +"Search or paste SimpleX link" = "Искать или вставить ссылку SimpleX"; + +/* No comment provided by engineer. */ +"Search videos" = "Поиск видео"; + +/* No comment provided by engineer. */ +"Search voice messages" = "Поиск голосовых сообщений"; /* network option */ "sec" = "сек"; @@ -4721,6 +5222,9 @@ chat item action */ /* chat item text */ "security code changed" = "код безопасности изменился"; +/* No comment provided by engineer. */ +"Security: owners hold channel keys." = "Безопасность: владельцы хранят ключи канала."; + /* chat item action */ "Select" = "Выбрать"; @@ -4749,7 +5253,7 @@ chat item action */ "Send" = "Отправить"; /* No comment provided by engineer. */ -"Send a live message - it will update for the recipient(s) as you type it" = "Отправить живое сообщение — оно будет обновляться для получателей по мере того, как Вы его вводите"; +"Send a live message - it will update for the recipient(s) as you type it" = "Отправить живое сообщение - оно будет обновляться для получателей по мере того, как Вы его вводите"; /* No comment provided by engineer. */ "Send contact request?" = "Отправить запрос на соединение?"; @@ -4758,7 +5262,7 @@ chat item action */ "Send delivery receipts to" = "Отправка отчётов о доставке"; /* No comment provided by engineer. */ -"Send direct message to connect" = "Отправьте сообщение чтобы соединиться"; +"Send direct message to connect" = "Отправить личное сообщение контакту"; /* No comment provided by engineer. */ "Send disappearing message" = "Отправить исчезающее сообщение"; @@ -4776,7 +5280,7 @@ chat item action */ "Send message to enable calls." = "Отправьте сообщение, чтобы включить звонки."; /* No comment provided by engineer. */ -"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Отправлять сообщения напрямую, когда IP адрес защищен, и Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку."; +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Отправлять сообщения напрямую, когда IP-адрес защищён, и Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку."; /* No comment provided by engineer. */ "Send messages directly when your or destination server does not support private routing." = "Отправлять сообщения напрямую, когда Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку."; @@ -4788,7 +5292,7 @@ chat item action */ "Send private reports" = "Вы можете сообщить о нарушениях"; /* No comment provided by engineer. */ -"Send questions and ideas" = "Отправьте вопросы и идеи"; +"Send questions and ideas" = "Вопросы и предложения"; /* No comment provided by engineer. */ "Send receipts" = "Отчёты о доставке"; @@ -4799,12 +5303,18 @@ chat item action */ /* No comment provided by engineer. */ "Send request without message" = "Отправить запрос без сообщения"; +/* No comment provided by engineer. */ +"Send the link via any messenger - it's secure. Ask to paste into SimpleX." = "Отправьте ссылку через любой мессенджер - это безопасно. Попросите вставить её в SimpleX."; + /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Отправьте из галереи или из дополнительных клавиатур."; /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Отправить до 100 последних сообщений новым членам."; +/* No comment provided by engineer. */ +"Send up to 100 last messages to new subscribers." = "Отправлять до 100 последних сообщений новым подписчикам."; + /* No comment provided by engineer. */ "Send your private feedback to groups." = "Отправляйте Ваши конфиденциальные предложения группе."; @@ -4814,6 +5324,9 @@ chat item action */ /* No comment provided by engineer. */ "Sender may have deleted the connection request." = "Отправитель мог удалить запрос на соединение."; +/* alert message */ +"Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later." = "Отправка картинки ссылки может раскрыть Ваш IP-адрес веб-сайту. Вы можете изменить это в настройках безопасности позже."; + /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Отправка отчётов о доставке будет включена для всех контактов во всех видимых профилях чата."; @@ -4881,7 +5394,7 @@ chat item action */ "Server address is incompatible with network settings." = "Адрес сервера несовместим с настройками сети."; /* alert title */ -"Server operator changed." = "Оператор серверов изменен."; +"Server operator changed." = "Оператор сервера изменен."; /* No comment provided by engineer. */ "Server operators" = "Операторы серверов"; @@ -4892,6 +5405,9 @@ chat item action */ /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "информация сервера об очереди: %1$@\n\nпоследнее полученное сообщение: %2$@"; +/* relay test error */ +"Server requires authorization to connect to relay, check password." = "Для подключения к релею требуется авторизация, проверьте пароль."; + /* server test error */ "Server requires authorization to create queues, check password." = "Сервер требует авторизации для создания очередей, проверьте пароль."; @@ -4976,6 +5492,12 @@ chat item action */ /* alert message */ "Settings were changed." = "Настройки были изменены."; +/* No comment provided by engineer. */ +"Setup notifications" = "Настроить уведомления"; + +/* No comment provided by engineer. */ +"Setup routers" = "Настроить серверы"; + /* No comment provided by engineer. */ "Shape profile images" = "Форма картинок профилей"; @@ -4996,7 +5518,10 @@ chat item action */ "Share address publicly" = "Поделитесь адресом"; /* alert title */ -"Share address with contacts?" = "Поделиться адресом с контактами?"; +"Share address with SimpleX contacts?" = "Поделиться адресом с контактами SimpleX?"; + +/* No comment provided by engineer. */ +"Share channel" = "Поделиться каналом"; /* No comment provided by engineer. */ "Share from other apps." = "Поделитесь из других приложений."; @@ -5013,6 +5538,9 @@ chat item action */ /* No comment provided by engineer. */ "Share profile" = "Поделиться профилем"; +/* No comment provided by engineer. */ +"Share relay address" = "Поделиться адресом релея"; + /* No comment provided by engineer. */ "Share SimpleX address on social media." = "Поделитесь SimpleX адресом в социальных сетях."; @@ -5023,7 +5551,10 @@ chat item action */ "Share to SimpleX" = "Поделиться в SimpleX"; /* No comment provided by engineer. */ -"Share with contacts" = "Поделиться с контактами"; +"Share via chat" = "Поделиться в чате"; + +/* No comment provided by engineer. */ +"Share with SimpleX contacts" = "Поделиться с контактами SimpleX"; /* No comment provided by engineer. */ "Share your address" = "Поделитесь Вашим адресом"; @@ -5044,7 +5575,7 @@ chat item action */ "Show calls in phone history" = "Показать звонки в истории телефона"; /* No comment provided by engineer. */ -"Show developer options" = "Показать опции для девелоперов"; +"Show developer options" = "Показать опции для разработчиков"; /* No comment provided by engineer. */ "Show last messages" = "Показывать последние сообщения"; @@ -5059,7 +5590,7 @@ chat item action */ "Show preview" = "Показывать уведомления"; /* No comment provided by engineer. */ -"Show QR code" = "Показать QR код"; +"Show QR code" = "Показать QR-код"; /* No comment provided by engineer. */ "Show:" = "Показать:"; @@ -5080,7 +5611,7 @@ chat item action */ "SimpleX address or 1-time link?" = "Адрес SimpleX или одноразовая ссылка?"; /* alert title */ -"SimpleX address settings" = "Настройки автоприема"; +"SimpleX address settings" = "Настройки автоприёма"; /* simplex link type */ "SimpleX channel link" = "SimpleX ссылка канала"; @@ -5101,7 +5632,7 @@ chat item action */ "SimpleX group link" = "SimpleX ссылка группы"; /* chat feature */ -"SimpleX links" = "SimpleX ссылки"; +"SimpleX links" = "Ссылки SimpleX"; /* No comment provided by engineer. */ "SimpleX links are prohibited." = "Ссылки SimpleX запрещены в этой группе."; @@ -5128,10 +5659,10 @@ chat item action */ "SimpleX protocols reviewed by Trail of Bits." = "Аудит SimpleX протоколов от Trail of Bits."; /* simplex link type */ -"SimpleX relay link" = "Ссылка SimpleX relay"; +"SimpleX relay address" = "Адрес релея SimpleX"; /* No comment provided by engineer. */ -"Simplified incognito mode" = "Упрощенный режим Инкогнито"; +"Simplified incognito mode" = "Упрощённый режим Инкогнито"; /* No comment provided by engineer. */ "Size" = "Размер"; @@ -5146,10 +5677,10 @@ chat item action */ "Small groups (max 20)" = "Маленькие группы (до 20)"; /* No comment provided by engineer. */ -"SMP server" = "SMP сервер"; +"SMP server" = "SMP-сервер"; /* No comment provided by engineer. */ -"SOCKS proxy" = "SOCKS прокси"; +"SOCKS proxy" = "SOCKS-прокси"; /* blur media */ "Soft" = "Слабое"; @@ -5180,7 +5711,10 @@ report reason */ "Square, circle, or anything in between." = "Квадрат, круг и все, что между ними."; /* chat item text */ -"standard end-to-end encryption" = "стандартное end-to-end шифрование"; +"standard end-to-end encryption" = "стандартное сквозное шифрование"; + +/* No comment provided by engineer. */ +"Star on GitHub" = "Поставить звёздочку на GitHub"; /* No comment provided by engineer. */ "Start chat" = "Запустить чат"; @@ -5248,6 +5782,48 @@ report reason */ /* No comment provided by engineer. */ "Subscribed" = "Подписано"; +/* No comment provided by engineer. */ +"Subscriber" = "Подписчик"; + +/* chat feature */ +"Subscriber reports" = "Сообщения о нарушениях"; + +/* alert message */ +"Subscriber will be removed from channel - this cannot be undone!" = "Подписчик будет удалён из канала - это нельзя отменить!"; + +/* No comment provided by engineer. */ +"Subscribers" = "Подписчики"; + +/* No comment provided by engineer. */ +"Subscribers can add message reactions." = "Подписчики могут добавлять реакции на сообщения."; + +/* No comment provided by engineer. */ +"Subscribers can chat with admins." = "Подписчики могут общаться с админами."; + +/* No comment provided by engineer. */ +"Subscribers can irreversibly delete sent messages. (24 hours)" = "Подписчики могут необратимо удалять отправленные сообщения. (24 часа)"; + +/* No comment provided by engineer. */ +"Subscribers can report messsages to moderators." = "Подписчики могут отправлять сообщения о нарушениях модераторам."; + +/* No comment provided by engineer. */ +"Subscribers can send direct messages." = "Подписчики могут отправлять личные сообщения."; + +/* No comment provided by engineer. */ +"Subscribers can send disappearing messages." = "Подписчики могут отправлять исчезающие сообщения."; + +/* No comment provided by engineer. */ +"Subscribers can send files and media." = "Подписчики могут отправлять файлы и медиа."; + +/* No comment provided by engineer. */ +"Subscribers can send SimpleX links." = "Подписчики могут отправлять ссылки SimpleX."; + +/* No comment provided by engineer. */ +"Subscribers can send voice messages." = "Подписчики могут отправлять голосовые сообщения."; + +/* No comment provided by engineer. */ +"Subscribers use relay link to connect to the channel.\nRelay address was used to set up this relay for the channel." = "Подписчики используют ссылку релея для подключения к каналу.\nАдрес релея был использован для настройки этого релея для канала."; + /* No comment provided by engineer. */ "Subscription errors" = "Ошибки подписки"; @@ -5275,6 +5851,9 @@ report reason */ /* No comment provided by engineer. */ "Take picture" = "Сделать фото"; +/* No comment provided by engineer. */ +"Talk to someone" = "Начните разговор"; + /* No comment provided by engineer. */ "Tap button " = "Нажмите кнопку "; @@ -5288,16 +5867,16 @@ report reason */ "Tap Connect to use bot" = "Нажмите Соединиться, чтобы использовать бот"; /* No comment provided by engineer. */ -"Tap Create SimpleX address in the menu to create it later." = "Нажмите Создать адрес SimpleX в меню, чтобы создать его позже."; +"Tap Join channel" = "Нажмите Войти в канал"; /* No comment provided by engineer. */ "Tap Join group" = "Нажмите Вступить в группу"; /* No comment provided by engineer. */ -"Tap to activate profile." = "Нажмите, чтобы сделать профиль активным."; +"Tap to activate profile." = "Нажмите на профиль, чтобы переключиться."; /* No comment provided by engineer. */ -"Tap to Connect" = "Нажмите чтобы соединиться"; +"Tap to Connect" = "Нажмите, чтобы соединиться"; /* No comment provided by engineer. */ "Tap to join" = "Нажмите, чтобы вступить"; @@ -5305,6 +5884,9 @@ report reason */ /* No comment provided by engineer. */ "Tap to join incognito" = "Нажмите, чтобы вступить инкогнито"; +/* No comment provided by engineer. */ +"Tap to open" = "Нажмите, чтобы открыть"; + /* No comment provided by engineer. */ "Tap to paste link" = "Нажмите, чтобы вставить ссылку"; @@ -5318,7 +5900,7 @@ report reason */ "TCP connection bg timeout" = "Фоновый таймаут TCP-соединения"; /* No comment provided by engineer. */ -"TCP connection timeout" = "Таймаут TCP соединения"; +"TCP connection timeout" = "Таймаут TCP-соединения"; /* No comment provided by engineer. */ "TCP port for messaging" = "TCP-порт для отправки сообщений"; @@ -5335,12 +5917,16 @@ report reason */ /* file error alert title */ "Temporary file error" = "Временная ошибка файла"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Ошибка теста на шаге %@."; /* No comment provided by engineer. */ "Test notifications" = "Протестировать уведомления"; +/* No comment provided by engineer. */ +"Test relay" = "Тест релея"; + /* No comment provided by engineer. */ "Test server" = "Тестировать сервер"; @@ -5368,6 +5954,9 @@ report reason */ /* No comment provided by engineer. */ "The app protects your privacy by using different operators in each conversation." = "Приложение улучшает конфиденциальность используя разных операторов в каждом разговоре."; +/* No comment provided by engineer. */ +"The app removed this message after %lld attempts to receive it." = "Приложение удалило это сообщение после %lld попыток его получить."; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "Приложение будет запрашивать подтверждение загрузки с неизвестных серверов (за исключением .onion адресов)."; @@ -5375,7 +5964,10 @@ report reason */ "The attempt to change database passphrase was not completed." = "Попытка поменять пароль базы данных не была завершена."; /* No comment provided by engineer. */ -"The code you scanned is not a SimpleX link QR code." = "Этот QR код не является SimpleX-ccылкой."; +"The code you scanned is not a SimpleX link QR code." = "Этот QR-код не является SimpleX-ccылкой."; + +/* conn error description */ +"The connection reached the limit of undelivered messages" = "Соединение достигло лимита недоставленных сообщений"; /* No comment provided by engineer. */ "The connection reached the limit of undelivered messages, your contact may be offline." = "Соединение достигло предела недоставленных сообщений. Возможно, Ваш контакт не в сети."; @@ -5393,7 +5985,7 @@ report reason */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Шифрование работает, и новое соглашение не требуется. Это может привести к ошибкам соединения!"; /* No comment provided by engineer. */ -"The future of messaging" = "Будущее коммуникаций"; +"The first network where you own\nyour contacts and groups." = "Первая сеть, в которой Вы владеете\nсвоими контактами и группами."; /* No comment provided by engineer. */ "The hash of the previous message is different." = "Хэш предыдущего сообщения отличается."; @@ -5408,19 +6000,22 @@ report reason */ "The message will be deleted for all members." = "Сообщение будет удалено для всех членов группы."; /* No comment provided by engineer. */ -"The message will be marked as moderated for all members." = "Сообщение будет помечено как удаленное для всех членов группы."; +"The message will be marked as moderated for all members." = "Сообщение будет помечено как удалённое для всех членов группы."; /* No comment provided by engineer. */ "The messages will be deleted for all members." = "Сообщения будут удалены для всех членов группы."; /* No comment provided by engineer. */ -"The messages will be marked as moderated for all members." = "Сообщения будут помечены как удаленные для всех членов группы."; +"The messages will be marked as moderated for all members." = "Сообщения будут помечены как удалённые для всех членов группы."; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Предыдущая версия данных чата не удалена при перемещении, её можно удалить."; /* No comment provided by engineer. */ -"The same conditions will apply to operator **%@**." = "Те же самые условия будут приняты для оператора **%@**."; +"The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it." = "Древнейшая человеческая свобода - говорить с другим человеком без слежки - построенная на инфраструктуре, которая не может её предать."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Те же условия будут действовать для оператора **%s**."; /* No comment provided by engineer. */ "The second preset operator in the app!" = "Второй оператор серверов в приложении!"; @@ -5441,11 +6036,17 @@ report reason */ "The text you pasted is not a SimpleX link." = "Вставленный текст не является SimpleX-ссылкой."; /* No comment provided by engineer. */ -"The uploaded database archive will be permanently removed from the servers." = "Загруженный архив базы данных будет навсегда удален с серверов."; +"The uploaded database archive will be permanently removed from the servers." = "Загруженный архив базы данных будет навсегда удалён с серверов."; /* No comment provided by engineer. */ "Themes" = "Темы"; +/* No comment provided by engineer. */ +"Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible." = "Потом мы вышли в интернет, и каждая платформа попросила частичку вас - ваше имя, ваш номер, ваших друзей. Мы смирились с тем, что за возможность общаться приходится отдавать информацию о том, с кем мы общаемся. Каждое поколение людей и технологий жило так - телефон, электронная почта, мессенджеры, социальные сети. Казалось, что другого пути нет."; + +/* No comment provided by engineer. */ +"There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected." = "Другой путь есть. Сеть без номеров телефонов. Без имён пользователей. Без аккаунтов. Без каких-либо идентификаторов пользователей. Сеть, которая соединяет людей и передаёт зашифрованные сообщения, не зная, кто с кем связан."; + /* No comment provided by engineer. */ "These conditions will also apply for: **%@**." = "Эти условия также будут применены к: **%@**."; @@ -5453,25 +6054,25 @@ report reason */ "These settings are for your current profile **%@**." = "Установки для Вашего активного профиля **%@**."; /* No comment provided by engineer. */ -"They can be overridden in contact and group 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." = "Это действие нельзя отменить — все полученные и отправленные файлы будут удалены. Изображения останутся в низком разрешении."; +"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." = "Это действие нельзя отменить — все сообщения, отправленные или полученные раньше чем выбрано, будут удалены. Это может занять несколько минут."; +"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Это действие нельзя отменить - все сообщения, отправленные или полученные раньше чем выбрано, будут удалены. Это может занять несколько минут."; /* alert message */ "This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Это действие нельзя отменить - сообщения в этом чате, отправленные или полученные раньше чем выбрано, будут удалены."; /* No comment provided by engineer. */ -"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Это действие нельзя отменить — Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны."; +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Это действие нельзя отменить - Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны."; /* E2EE info chat item */ -"This chat is protected by end-to-end encryption." = "Чат защищен end-to-end шифрованием."; +"This chat is protected by end-to-end encryption." = "Чат защищён сквозным шифрованием."; /* E2EE info chat item */ -"This chat is protected by quantum resistant end-to-end encryption." = "Чат защищен квантово-устойчивым end-to-end шифрованием."; +"This chat is protected by quantum resistant end-to-end encryption." = "Чат защищён квантово-устойчивым сквозным шифрованием."; /* notification title */ "this contact" = "этот контакт"; @@ -5488,6 +6089,12 @@ report reason */ /* No comment provided by engineer. */ "This group no longer exists." = "Эта группа больше не существует."; +/* alert message */ +"This is a chat relay address, it cannot be used to connect." = "Это адрес чат-релея, с ним нельзя соединиться."; + +/* new chat action */ +"This is your link for channel %@!" = "Это ваша ссылка на канал %@!"; + /* No comment provided by engineer. */ "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Эта ссылка требует новую версию. Обновите приложение или попросите Ваш контакт прислать совместимую ссылку."; @@ -5495,7 +6102,7 @@ report reason */ "This link was used with another mobile device, please create a new link on the desktop." = "Эта ссылка была использована на другом мобильном, пожалуйста, создайте новую ссылку на компьютере."; /* No comment provided by engineer. */ -"This message was deleted or not received yet." = "Это сообщение было удалено или еще не получено."; +"This message was deleted or not received yet." = "Это сообщение было удалено или ещё не получено."; /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Эта настройка применяется к сообщениям в Вашем текущем профиле чата **%@**."; @@ -5521,6 +6128,9 @@ report reason */ /* No comment provided by engineer. */ "To make a new connection" = "Чтобы соединиться"; +/* No comment provided by engineer. */ +"To make SimpleX Network last." = "Чтобы сохранить сеть SimpleX для всех."; + /* No comment provided by engineer. */ "To protect against your link being replaced, you can compare contact security codes." = "Чтобы защитить Вашу ссылку от замены, Вы можете сравнить код безопасности."; @@ -5531,10 +6141,10 @@ report reason */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Чтобы защитить Вашу информацию, включите блокировку SimpleX Chat.\nВам будет нужно пройти аутентификацию для включения блокировки."; /* No comment provided by engineer. */ -"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Чтобы защитить ваш IP адрес, приложение использует Ваши SMP серверы для конфиденциальной доставки сообщений."; +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Чтобы защитить Ваш IP-адрес, приложение использует Ваши SMP-серверы для конфиденциальной доставки сообщений."; /* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Чтобы защитить Вашу конфиденциальность, SimpleX использует разные идентификаторы для каждого Вашeго контакта."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Чтобы защитить Вашу конфиденциальность, SimpleX использует разные ID для каждого Вашего контакта."; /* No comment provided by engineer. */ "To receive" = "Для получения"; @@ -5567,10 +6177,7 @@ report reason */ "To use the servers of **%@**, accept conditions of use." = "Чтобы использовать серверы оператора **%@**, примите условия использования."; /* 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 chat list:" = "Переключите список чатов:"; +"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." = "Установите режим Инкогнито при соединении."; @@ -5581,17 +6188,20 @@ report reason */ /* No comment provided by engineer. */ "Toolbar opacity" = "Прозрачность тулбара"; +/* No comment provided by engineer. */ +"Top bar" = "Верхнее меню"; + /* No comment provided by engineer. */ "Total" = "Всего"; /* No comment provided by engineer. */ -"Transport isolation" = "Отдельные сессии для"; +"Transport isolation" = "Отдельные транспортные сессии"; /* No comment provided by engineer. */ "Transport sessions" = "Транспортные сессии"; /* subscription status explanation */ -"Trying to connect to the server used to receive messages from this connection." = "Попытка подключиться к серверу, используемому для получения сообщений от этого соединения."; +"Trying to connect to the server used to receive messages from this connection." = "Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта."; /* No comment provided by engineer. */ "Turkish interface" = "Турецкий интерфейс"; @@ -5620,6 +6230,9 @@ report reason */ /* No comment provided by engineer. */ "Unblock member?" = "Разблокировать члена группы?"; +/* No comment provided by engineer. */ +"Unblock subscriber for all?" = "Разблокировать подписчика для всех?"; + /* rcv group event chat item */ "unblocked %@" = "%@ разблокирован"; @@ -5669,7 +6282,7 @@ report reason */ "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Чтобы установить соединение, попросите Ваш контакт создать еще одну ссылку и проверьте Ваше соединение с сетью."; +"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. */ "Unlink" = "Забыть"; @@ -5692,12 +6305,15 @@ report reason */ /* swipe action */ "Unread" = "Не прочитано"; -/* No comment provided by engineer. */ +/* conn error description */ "Unsupported connection link" = "Ссылка не поддерживается"; /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "До 100 последних сообщений отправляются новым членам."; +/* No comment provided by engineer. */ +"Up to 100 last messages are sent to new subscribers." = "До 100 последних сообщений отправляется новым подписчикам."; + /* No comment provided by engineer. */ "Update" = "Обновить"; @@ -5710,8 +6326,11 @@ report reason */ /* No comment provided by engineer. */ "Update settings?" = "Обновить настройки?"; +/* rcv group event chat item */ +"updated channel profile" = "обновлён профиль канала"; + /* No comment provided by engineer. */ -"Updated conditions" = "Обновленные условия"; +"Updated conditions" = "Обновлённые условия"; /* rcv group event chat item */ "updated group profile" = "обновил(а) профиль группы"; @@ -5720,7 +6339,7 @@ report reason */ "updated profile" = "профиль обновлён"; /* No comment provided by engineer. */ -"Updating settings will re-connect the client to all servers." = "Обновление настроек приведет к сбросу и установке нового соединения со всеми серверами."; +"Updating settings will re-connect the client to all servers." = "Обновление настроек приведёт к сбросу и установке нового соединения со всеми серверами."; /* alert button */ "Upgrade" = "Обновить"; @@ -5767,9 +6386,6 @@ report reason */ /* No comment provided by engineer. */ "Use %@" = "Использовать %@"; -/* No comment provided by engineer. */ -"Use chat" = "Использовать чат"; - /* new chat action */ "Use current profile" = "Использовать активный профиль"; @@ -5779,6 +6395,9 @@ report reason */ /* No comment provided by engineer. */ "Use for messages" = "Использовать для сообщений"; +/* No comment provided by engineer. */ +"Use for new channels" = "Использовать для новых каналов"; + /* No comment provided by engineer. */ "Use for new connections" = "Использовать для новых соединений"; @@ -5792,17 +6411,20 @@ report reason */ "Use iOS call interface" = "Использовать интерфейс iOS для звонков"; /* new chat action */ -"Use new incognito profile" = "Использовать новый Инкогнито профиль"; +"Use new incognito profile" = "Использовать новый профиль инкогнито"; /* No comment provided by engineer. */ "Use only local notifications?" = "Использовать только локальные нотификации?"; /* No comment provided by engineer. */ -"Use private routing with unknown servers when IP address is not protected." = "Использовать конфиденциальную доставку с неизвестными серверами, когда IP адрес не защищен."; +"Use private routing with unknown servers when IP address is not protected." = "Использовать конфиденциальную доставку с неизвестными серверами, когда IP-адрес не защищён."; /* No comment provided by engineer. */ "Use private routing with unknown servers." = "Использовать конфиденциальную доставку с неизвестными серверами."; +/* No comment provided by engineer. */ +"Use relay" = "Использовать релей"; + /* No comment provided by engineer. */ "Use server" = "Использовать сервер"; @@ -5813,7 +6435,7 @@ report reason */ "Use SimpleX Chat servers?" = "Использовать серверы предосталенные SimpleX Chat?"; /* No comment provided by engineer. */ -"Use SOCKS proxy" = "Использовать SOCKS прокси"; +"Use SOCKS proxy" = "Использовать SOCKS-прокси"; /* No comment provided by engineer. */ "Use TCP port %@ when no port is specified." = "Использовать TCP-порт %@, когда порт не указан."; @@ -5827,6 +6449,9 @@ report reason */ /* No comment provided by engineer. */ "Use the app with one hand." = "Используйте приложение одной рукой."; +/* No comment provided by engineer. */ +"Use this address in your social media profile, website, or email signature." = "Используйте этот адрес в профиле социальных сетей, на сайте или в подписи email."; + /* No comment provided by engineer. */ "Use web port" = "Использовать веб-порт"; @@ -5845,6 +6470,9 @@ report reason */ /* No comment provided by engineer. */ "v%@ (%@)" = "v%@ (%@)"; +/* relay test step */ +"Verify" = "Проверить"; + /* No comment provided by engineer. */ "Verify code with desktop" = "Сверьте код с компьютером"; @@ -5866,6 +6494,9 @@ report reason */ /* No comment provided by engineer. */ "Verify security code" = "Подтвердить код безопасности"; +/* relay hostname */ +"via %@" = "через %@"; + /* No comment provided by engineer. */ "Via browser" = "В браузере"; @@ -5879,7 +6510,7 @@ report reason */ "via one-time link" = "через одноразовую ссылку"; /* No comment provided by engineer. */ -"via relay" = "через relay сервер"; +"via relay" = "через релей-сервер"; /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Через безопасный квантово-устойчивый протокол."; @@ -5899,6 +6530,9 @@ report reason */ /* 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" = "Видео"; + /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Видео и файлы до 1гб"; @@ -5932,9 +6566,18 @@ report reason */ /* No comment provided by engineer. */ "Voice messages prohibited!" = "Голосовые сообщения запрещены!"; +/* alert action */ +"Wait" = "Подождать"; + +/* relay test step */ +"Wait response" = "Ожидание ответа"; + /* No comment provided by engineer. */ "waiting for answer…" = "ожидается ответ…"; +/* No comment provided by engineer. */ +"Waiting for channel owner to add relays." = "Ожидает, когда владелец канала добавит релеи."; + /* No comment provided by engineer. */ "waiting for confirmation…" = "ожидается подтверждение…"; @@ -5942,10 +6585,10 @@ report reason */ "Waiting for desktop..." = "Ожидается подключение компьютера..."; /* No comment provided by engineer. */ -"Waiting for file" = "Ожидается прием файла"; +"Waiting for file" = "Ожидается приём файла"; /* No comment provided by engineer. */ -"Waiting for image" = "Ожидается прием изображения"; +"Waiting for image" = "Ожидается приём изображения"; /* No comment provided by engineer. */ "Waiting for video" = "Ожидание видео"; @@ -5960,13 +6603,16 @@ report reason */ "wants to connect to you!" = "хочет соединиться с Вами!"; /* No comment provided by engineer. */ -"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Внимание: запуск чата на нескольких устройствах не поддерживается и приведет к сбоям доставки сообщений"; +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Внимание: запуск чата на нескольких устройствах не поддерживается и приведёт к сбоям доставки сообщений"; /* No comment provided by engineer. */ -"Warning: you may lose some data!" = "Предупреждение: Вы можете потерять какие то данные!"; +"Warning: you may lose some data!" = "Предупреждение: Вы можете потерять некоторые данные!"; /* No comment provided by engineer. */ -"WebRTC ICE servers" = "WebRTC ICE серверы"; +"We made connecting simpler for new users." = "Мы упростили подключение для новых пользователей."; + +/* No comment provided by engineer. */ +"WebRTC ICE servers" = "WebRTC ICE-серверы"; /* time unit */ "weeks" = "недель"; @@ -5984,7 +6630,7 @@ report reason */ "Welcome your contacts 👋" = "Приветствуйте Ваши контакты 👋"; /* No comment provided by engineer. */ -"What's new" = "Новые функции"; +"What's new" = "Что нового"; /* No comment provided by engineer. */ "When available" = "Когда возможно"; @@ -5999,7 +6645,10 @@ report reason */ "When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Когда больше чем один оператор включен, ни один из них не видит метаданные, чтобы определить, кто соединен с кем."; /* 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." = "Когда Вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом."; +"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. */ +"Why SimpleX is built." = "Зачем создан SimpleX."; /* No comment provided by engineer. */ "WiFi" = "WiFi"; @@ -6020,10 +6669,10 @@ report reason */ "With reduced battery usage." = "С уменьшенным потреблением батареи."; /* No comment provided by engineer. */ -"Without Tor or VPN, your IP address will be visible to file servers." = "Без Тора или ВПН, Ваш IP адрес будет доступен серверам файлов."; +"Without Tor or VPN, your IP address will be visible to file servers." = "Без Tor или VPN, Ваш IP-адрес будет доступен серверам файлов."; /* alert message */ -"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Без Тора или ВПН, Ваш IP адрес будет доступен этим серверам файлов: %@."; +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Без Тора или ВПН, Ваш IP-адрес будет доступен этим серверам файлов: %@."; /* No comment provided by engineer. */ "Wrong database passphrase" = "Неправильный пароль базы данных"; @@ -6032,13 +6681,13 @@ report reason */ "Wrong key or unknown connection - most likely this connection is deleted." = "Неверный ключ или неизвестное соединение - скорее всего, это соединение удалено."; /* file error text */ -"Wrong key or unknown file chunk address - most likely file is deleted." = "Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удален."; +"Wrong key or unknown file chunk address - most likely file is deleted." = "Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удалён."; /* No comment provided by engineer. */ "Wrong passphrase!" = "Неправильный пароль!"; /* No comment provided by engineer. */ -"XFTP server" = "XFTP сервер"; +"XFTP server" = "XFTP-сервер"; /* pref value */ "yes" = "да"; @@ -6053,7 +6702,7 @@ report reason */ "You accepted connection" = "Вы приняли приглашение соединиться"; /* snd group event chat item */ -"you accepted this member" = "Вы приняли этого члена"; +"you accepted this member" = "Вы приняли этого члена группы"; /* No comment provided by engineer. */ "You allow" = "Вы разрешаете"; @@ -6086,13 +6735,13 @@ report reason */ "You are already joining the group!\nRepeat join request?" = "Вы уже вступаете в группу!\nПовторить запрос на вступление?"; /* subscription status explanation */ -"You are connected to the server used to receive messages from this connection." = "Вы подключены к серверу, используемому для приема сообщений от этого соединения."; +"You are connected to the server used to receive messages from this connection." = "Вы подключены к серверу, используемому для приёма сообщений от этого соединения."; /* No comment provided by engineer. */ "You are invited to group" = "Вы приглашены в группу"; /* subscription status explanation */ -"You are not connected to the server used to receive messages from this connection (no subscription)." = "Вы не подключены к серверу, используемому для получения сообщений по этому соединению (нет подписки)."; +"You are not connected to the server used to receive messages from this connection (no subscription)." = "Вы не подключены к серверу, через который Вы получали сообщения от этого контакта (нет подписки)."; /* No comment provided by engineer. */ "You are not connected to these servers. Private routing is used to deliver messages to them." = "Вы не подключены к этим серверам. Для доставки сообщений на них используется конфиденциальная доставка."; @@ -6100,6 +6749,9 @@ report reason */ /* No comment provided by engineer. */ "you are observer" = "только чтение сообщений"; +/* No comment provided by engineer. */ +"you are subscriber" = "Вы подписчик"; + /* snd group event chat item */ "you blocked %@" = "Вы заблокировали %@"; @@ -6122,7 +6774,7 @@ report reason */ "You can enable them later via app Privacy & Security settings." = "Вы можете включить их позже в настройках Конфиденциальности."; /* No comment provided by engineer. */ -"You can give another try." = "Вы можете попробовать еще раз."; +"You can give another try." = "Вы можете попробовать ещё раз."; /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "Вы можете скрыть профиль или выключить уведомления - потяните его вправо."; @@ -6143,13 +6795,16 @@ report reason */ "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 кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились."; +"You can share a link or a QR code - anybody will be able to join the channel." = "Вы можете поделиться ссылкой или QR-кодом - любой сможет вступить в канал."; + +/* 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 start chat via app Settings / Database or by restarting the app" = "Вы можете запустить чат через Настройки приложения или перезапустив приложение."; +"You can start chat via app Settings / Database or by restarting the app" = "Вы можете запустить чат через Настройки приложения или перезапустив приложение"; /* No comment provided by engineer. */ "You can still view conversation with %@ in the list of chats." = "Вы по-прежнему можете просмотреть разговор с %@ в списке чатов."; @@ -6182,16 +6837,19 @@ report reason */ "you changed role of %@ to %@" = "Вы поменяли роль члена %1$@ на: %2$@"; /* No comment provided by engineer. */ -"You could not be verified; please try again." = "Верификация не удалась; пожалуйста, попробуйте ещё раз."; +"You commit to:\n- Only legal content in public groups\n- Respect other users - no spam" = "Вы обязуетесь:\n- Только законный контент в публичных группах\n- Уважать других пользователей - без спама"; /* No comment provided by engineer. */ -"You decide who can connect." = "Вы определяете, кто может соединиться."; +"You connected to the channel via this relay link." = "Вы подключились к каналу через эту ссылку релея."; + +/* No comment provided by engineer. */ +"You could not be verified; please try again." = "Ошибка аутентификации; попробуйте ещё раз."; /* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Вы уже запросили соединение!\nПовторить запрос?"; /* No comment provided by engineer. */ -"You have to enter passphrase every time the app starts - it is not stored on the device." = "Пароль не сохранен на устройстве — Вы будете должны ввести его при каждом запуске чата."; +"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" = "Вы пригласили контакт"; @@ -6200,7 +6858,7 @@ report reason */ "You joined this group" = "Вы вступили в эту группу"; /* No comment provided by engineer. */ -"You joined this group. Connecting to inviting group member." = "Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы."; +"You joined this group. Connecting to inviting group member." = "Вы вступили в группу. Устанавливается соединение с пригласившим Вас членом группы."; /* snd group event chat item */ "you left" = "Вы покинули группу"; @@ -6212,7 +6870,7 @@ report reason */ "You may save the exported archive." = "Вы можете сохранить экспортированный архив."; /* 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." = "Вы должны всегда использовать самую новую версию данных чата, ТОЛЬКО на одном устройстве, иначе Вы можете перестать получать сообщения от каких то контактов."; +"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 call to be able to call them." = "Чтобы включить звонки, разрешите их Вашему контакту."; @@ -6241,6 +6899,9 @@ report reason */ /* snd group event chat item */ "you unblocked %@" = "Вы разблокировали %@"; +/* No comment provided by engineer. */ +"You were born without an account" = "Вы родились без аккаунта"; + /* No comment provided by engineer. */ "You will be able to send messages **only after your request is accepted**." = "Вы сможете отправлять сообщения **только после того как Ваш запрос будет принят**."; @@ -6260,7 +6921,10 @@ report reason */ "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 still receive calls and notifications from muted profiles when they are active." = "Вы все равно получите звонки и уведомления в профилях без звука, когда они активные."; +"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 channel. Chat history will be preserved." = "Вы перестанете получать сообщения из этого канала. История чата сохранится."; /* No comment provided by engineer. */ "You will stop receiving messages from this chat. Chat history will be preserved." = "Вы прекратите получать сообщения в этом разговоре. История будет сохранена."; @@ -6269,23 +6933,26 @@ report reason */ "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." = "Вы сможете удалить адрес, сохранив контакты, которые через него соединились."; +"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" = "Вы пытаетесь пригласить инкогнито контакт в группу, где Вы используете свой основной профиль"; +"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" = "Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие Вашего основного профиля, приглашать контакты не разрешено"; +"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 business contact" = "Ваш бизнес контакт"; +"Your business contact" = "Ваш бизнес-контакт"; /* No comment provided by engineer. */ "Your calls" = "Ваши звонки"; +/* No comment provided by engineer. */ +"Your channel" = "Ваш канал"; + /* No comment provided by engineer. */ "Your chat database" = "База данных"; @@ -6317,7 +6984,10 @@ report reason */ "Your contacts will remain connected." = "Ваши контакты сохранятся."; /* No comment provided by engineer. */ -"Your credentials may be sent unencrypted." = "Ваши учетные данные могут быть отправлены в незашифрованном виде."; +"Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public." = "Ваши разговоры принадлежат вам, как это всегда было до интернета. Сеть - это не место, куда вы приходите. Это место, которое вы создаёте и которым владеете. И никто не может это у вас отнять, делаете ли вы его конфиденциальным или публичным."; + +/* No comment provided by engineer. */ +"Your credentials may be sent unencrypted." = "Ваши учётные данные могут быть отправлены в незашифрованном виде."; /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Текущие данные Вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными."; @@ -6329,7 +6999,10 @@ report reason */ "Your group" = "Ваша группа"; /* No comment provided by engineer. */ -"Your ICE servers" = "Ваши ICE серверы"; +"Your ICE servers" = "Ваши ICE-серверы"; + +/* No comment provided by engineer. */ +"Your network" = "Ваша сеть"; /* No comment provided by engineer. */ "Your preferences" = "Ваши предпочтения"; @@ -6340,6 +7013,9 @@ report reason */ /* No comment provided by engineer. */ "Your profile" = "Ваш профиль"; +/* No comment provided by engineer. */ +"Your profile **%@** will be shared with channel relays and subscribers.\nRelays can access channel messages." = "Ваш профиль **%@** будет отправлен чат-релеям и подписчикам канала.\nРелеи могут видеть сообщения канала."; + /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Будет отправлен Ваш профиль **%@**."; @@ -6347,14 +7023,23 @@ report reason */ "Your profile is stored on your device and only shared with your contacts." = "Ваш профиль хранится на Вашем устройстве и отправляется только контактам."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. SimpleX серверы не могут получить доступ к Вашему профилю."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. Серверы SimpleX не могут получить доступ к Вашему профилю."; /* alert message */ -"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Ваш профиль был изменен. Если вы сохраните его, обновленный профиль будет отправлен всем вашим контактам."; +"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Ваш профиль был изменен. Если вы сохраните его, обновлённый профиль будет отправлен всем вашим контактам."; + +/* No comment provided by engineer. */ +"Your public address" = "Ваш публичный адрес"; /* No comment provided by engineer. */ "Your random profile" = "Случайный профиль"; +/* No comment provided by engineer. */ +"Your relay address" = "Ваш адрес релея"; + +/* No comment provided by engineer. */ +"Your relay name" = "Ваше имя релея"; + /* No comment provided by engineer. */ "Your server address" = "Адрес Вашего сервера"; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index d4b4dfd949..652737b4ca 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -13,15 +13,9 @@ /* No comment provided by engineer. */ "!1 colored!" = "!1 มีสี!"; -/* 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. */ "**e2e encrypted** audio call" = "การโทรเสียงแบบ **encrypted จากต้นจนจบ**"; @@ -233,9 +227,6 @@ swipe action */ /* 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 profile" = "เพิ่มโปรไฟล์"; @@ -362,9 +353,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "รับสาย"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้"; - /* No comment provided by engineer. */ "App build: %@" = "รุ่นแอป: %@"; @@ -626,7 +614,8 @@ set passcode view */ /* No comment provided by engineer. */ "Confirm password" = "ยืนยันรหัสผ่าน"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "เชื่อมต่อ"; /* No comment provided by engineer. */ @@ -671,7 +660,7 @@ set passcode view */ /* alert title */ "Connection error" = "การเชื่อมต่อผิดพลาด"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "การเชื่อมต่อผิดพลาด (AUTH)"; /* chat list item title (it should not be shown */ @@ -719,15 +708,15 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "ดำเนินการต่อ"; +/* No comment provided by engineer. */ +"Contribute" = "มีส่วนร่วม"; + /* No comment provided by engineer. */ "Copy" = "คัดลอก"; /* No comment provided by engineer. */ "Core version: v%@" = "รุ่นหลัก: v%@"; -/* No comment provided by engineer. */ -"Create" = "สร้าง"; - /* server test step */ "Create file" = "สร้างไฟล์"; @@ -827,9 +816,6 @@ set passcode view */ /* time unit */ "days" = "วัน"; -/* No comment provided by engineer. */ -"Decentralized" = "กระจายอำนาจแล้ว"; - /* message decrypt error item */ "Decryption error" = "ข้อผิดพลาดในการ decrypt"; @@ -1055,7 +1041,7 @@ alert button */ /* No comment provided by engineer. */ "Edit group profile" = "แก้ไขโปรไฟล์กลุ่ม"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "เปิดใช้งาน"; /* No comment provided by engineer. */ @@ -1073,9 +1059,6 @@ alert button */ /* No comment provided by engineer. */ "Enable lock" = "เปิดใช้งานการล็อค"; -/* No comment provided by engineer. */ -"Enable notifications" = "เปิดใช้งานการแจ้งเตือน"; - /* No comment provided by engineer. */ "Enable periodic notifications?" = "เปิดใช้การแจ้งเตือนเป็นระยะๆ ไหม?"; @@ -1181,7 +1164,7 @@ alert button */ /* No comment provided by engineer. */ "error" = "ผิดพลาด"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "ผิดพลาด"; /* No comment provided by engineer. */ @@ -1381,7 +1364,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "ค้นหาแชทได้เร็วขึ้น"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง"; /* No comment provided by engineer. */ @@ -1447,7 +1431,7 @@ snd error text */ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "คำเชิญเข้าร่วมกลุ่มใช้ไม่ถูกต้องอีกต่อไป คำเชิญถูกลบโดยผู้ส่ง"; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "ลิงค์กลุ่ม"; /* No comment provided by engineer. */ @@ -1549,9 +1533,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "โดยทันที"; -/* No comment provided by engineer. */ -"Immune to spam" = "มีภูมิคุ้มกันต่อสแปมและการละเมิด"; - /* No comment provided by engineer. */ "Import" = "นำเข้า"; @@ -1613,7 +1594,7 @@ snd error text */ "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)"; +"Install SimpleX Chat for terminal" = "ติดตั้ง SimpleX Chat สำหรับเทอร์มินัล"; /* No comment provided by engineer. */ "Instant" = "ทันที"; @@ -1630,7 +1611,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "ข้อมูลแชทไม่ถูกต้อง"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "ลิงค์เชื่อมต่อไม่ถูกต้อง"; /* invalid chat item */ @@ -2002,9 +1983,6 @@ snd error text */ /* copied message info in history */ "no text" = "ไม่มีข้อความ"; -/* No comment provided by engineer. */ -"No user identifiers." = "แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว"; - /* No comment provided by engineer. */ "Notifications" = "การแจ้งเตือน"; @@ -2196,9 +2174,6 @@ new chat action */ /* No comment provided by engineer. */ "Privacy & security" = "ความเป็นส่วนตัวและความปลอดภัย"; -/* No comment provided by engineer. */ -"Privacy redefined" = "นิยามความเป็นส่วนตัวใหม่"; - /* No comment provided by engineer. */ "Private filenames" = "ชื่อไฟล์ส่วนตัว"; @@ -2211,9 +2186,6 @@ new chat action */ /* No comment provided by engineer. */ "Profile password" = "รหัสผ่านโปรไฟล์"; -/* alert message */ -"Profile update will be sent to your contacts." = "การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ"; - /* No comment provided by engineer. */ "Prohibit audio/video calls." = "ห้ามการโทรด้วยเสียง/วิดีโอ"; @@ -2266,13 +2238,10 @@ new chat action */ "Read more" = "อ่านเพิ่มเติม"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)"; +"Read more in our GitHub repository." = "อ่านเพิ่มเติมในพื้นที่เก็บข้อมูล GitHub"; /* 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)"; +"Read more in User Guide." = "อ่านเพิ่มเติมในคู่มือผู้ใช้"; /* No comment provided by engineer. */ "received answer…" = "ได้รับคำตอบ…"; @@ -2623,15 +2592,9 @@ chat item action */ /* No comment provided by engineer. */ "Share address" = "แชร์ที่อยู่"; -/* alert title */ -"Share address with contacts?" = "แชร์ที่อยู่กับผู้ติดต่อ?"; - /* No comment provided by engineer. */ "Share link" = "แชร์ลิงก์"; -/* No comment provided by engineer. */ -"Share with contacts" = "แชร์กับผู้ติดต่อ"; - /* No comment provided by engineer. */ "Show calls in phone history" = "แสดงการโทรในประวัติการโทร"; @@ -2692,6 +2655,9 @@ chat item action */ /* notification title */ "Somebody" = "ใครบางคน"; +/* No comment provided by engineer. */ +"Star on GitHub" = "ติดดาวบน GitHub"; + /* No comment provided by engineer. */ "Start chat" = "เริ่มแชท"; @@ -2770,7 +2736,8 @@ chat item action */ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "การทดสอบล้มเหลวในขั้นตอน %@"; /* No comment provided by engineer. */ @@ -2809,9 +2776,6 @@ chat item action */ /* 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 future of messaging" = "การส่งข้อความส่วนตัวรุ่นต่อไป"; - /* No comment provided by engineer. */ "The hash of the previous message is different." = "แฮชของข้อความก่อนหน้านี้แตกต่างกัน"; @@ -2971,9 +2935,6 @@ chat item action */ /* 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" = "ใช้สำหรับการเชื่อมต่อใหม่"; @@ -3172,9 +3133,6 @@ chat item action */ /* No comment provided by engineer. */ "You could not be verified; please try again." = "เราไม่สามารถตรวจสอบคุณได้ กรุณาลองอีกครั้ง."; -/* No comment provided by engineer. */ -"You decide who can connect." = "ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "คุณต้องใส่รหัสผ่านทุกครั้งที่เริ่มแอป - รหัสผ่านไม่ได้จัดเก็บไว้ในอุปกรณ์"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 5cccb67170..fb3bf39168 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -25,15 +25,9 @@ /* No comment provided by engineer. */ "(this device v%@)" = "(bu cihaz v%@)"; -/* No comment provided by engineer. */ -"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Katkıda bulun](https://github.com/simplex-chat/simplex-chat#contribute)"; - /* No comment provided by engineer. */ "[Send us email](mailto:chat@simplex.chat)" = "[Bize e-posta gönder](mailto:chat@simplex.chat)"; -/* No comment provided by engineer. */ -"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Bize GitHub'da yıldız verin](https://github.com/simplex-chat/simplex-chat)"; - /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Kişi ekle**: yeni bir davet bağlantısı oluşturmak için, ya da aldığın bağlantıyla bağlan."; @@ -398,9 +392,6 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "Aktif bağlantılar"; -/* 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." = "Kişilerinizin başkalarıyla paylaşabilmesi için profilinize adres ekleyin. Profil güncellemesi kişilerinize gönderilecek."; - /* No comment provided by engineer. */ "Add friends" = "Arkadaş ekle"; @@ -647,9 +638,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Aramayı cevapla"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "Açık kaynak protokolü ve kodu - herhangi biri sunucuları çalıştırabilir."; - /* No comment provided by engineer. */ "App build: %@" = "Uygulama sürümü: %@"; @@ -891,7 +879,7 @@ marked deleted chat item preview text */ /* 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)!" = "Bulgarca, Fince, Tayca ve Ukraynaca - kullanıcılara ve [Weblate] e teşekkürler! (https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; -/* No comment provided by engineer. */ +/* chat link info line */ "Business address" = "İş adresi"; /* No comment provided by engineer. */ @@ -906,9 +894,6 @@ marked deleted chat item preview text */ /* 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)." = "Sohbet profiline göre (varsayılan) veya [bağlantıya göre](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; -/* No comment provided by engineer. */ -"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "SimpleX Chat'i kullanarak şunları kabul etmiş olursunuz:\n- herkese açık gruplarda yalnızca yasal içerik göndermek.\n- diğer kullanıcılara saygı göstermek – spam yapmamak."; - /* No comment provided by engineer. */ "call" = "Ara"; @@ -1092,7 +1077,8 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "Sohbet senden silinecek - bu geri alınamaz!"; -/* chat toolbar */ +/* chat feature +chat toolbar */ "Chat with admins" = "Yöneticilerle sohbet et"; /* No comment provided by engineer. */ @@ -1206,9 +1192,6 @@ set passcode view */ /* No comment provided by engineer. */ "Configure ICE servers" = "ICE sunucularını ayarla"; -/* No comment provided by engineer. */ -"Configure server operators" = "Sunucu operatörlerini yapılandır"; - /* No comment provided by engineer. */ "Confirm" = "Onayla"; @@ -1242,7 +1225,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Onaylandı"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Bağlan"; /* No comment provided by engineer. */ @@ -1341,7 +1325,7 @@ set passcode view */ /* alert title */ "Connection error" = "Bağlantı hatası"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "Bağlantı hatası (DOĞRULAMA)"; /* chat list item title (it should not be shown */ @@ -1443,6 +1427,9 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "Devam et"; +/* No comment provided by engineer. */ +"Contribute" = "Katkıda bulun"; + /* No comment provided by engineer. */ "Conversation deleted!" = "Sohbet silindi!"; @@ -1458,12 +1445,9 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Köşeleri yuvarlama"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "İsim %@ olarak düzeltilsin mi?"; -/* No comment provided by engineer. */ -"Create" = "Oluştur"; - /* No comment provided by engineer. */ "Create 1-time link" = "Tek kullanımlık bağlantı oluştur"; @@ -1617,9 +1601,6 @@ set passcode view */ /* No comment provided by engineer. */ "Debug delivery" = "Hata ayıklama teslimatı"; -/* No comment provided by engineer. */ -"Decentralized" = "Merkezi Olmayan"; - /* message decrypt error item */ "Decryption error" = "Şifre çözme hatası"; @@ -2017,7 +1998,7 @@ chat item action */ /* No comment provided by engineer. */ "Empty message!" = "Boş mesaj!"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "Etkinleştir"; /* No comment provided by engineer. */ @@ -2047,9 +2028,6 @@ chat item action */ /* No comment provided by engineer. */ "Enable lock" = "Kilidi etkinleştir"; -/* No comment provided by engineer. */ -"Enable notifications" = "Bildirimleri etkinleştir"; - /* No comment provided by engineer. */ "Enable periodic notifications?" = "Periyodik bildirimler etkinleştirilsin mi?"; @@ -2191,7 +2169,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "hata"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "Hata"; /* No comment provided by engineer. */ @@ -2323,9 +2301,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Kişiyi hazırlama hatası"; -/* No comment provided by engineer. */ -"Error opening group" = "Grubu hazırlama hatası"; - /* alert title */ "Error receiving file" = "Dosya alınırken sorun oluştu"; @@ -2574,7 +2549,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Sohbetleri daha hızlı bul"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil"; /* No comment provided by engineer. */ @@ -2598,7 +2574,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Tüm moderatörler için"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "Sohbet profili için %@:"; /* No comment provided by engineer. */ @@ -2730,7 +2707,7 @@ snd error text */ /* No comment provided by engineer. */ "group is deleted" = "grup silindi"; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "Grup bağlantısı"; /* No comment provided by engineer. */ @@ -2856,9 +2833,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "Hemen"; -/* No comment provided by engineer. */ -"Immune to spam" = "Spam ve kötüye kullanıma karşı bağışıklı"; - /* No comment provided by engineer. */ "Import" = "İçe aktar"; @@ -2959,7 +2933,7 @@ snd error text */ "Initial role" = "Başlangıç rolü"; /* No comment provided by engineer. */ -"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "[Terminal için SimpleX Chat]i indir(https://github.com/simplex-chat/simplex-chat)"; +"Install SimpleX Chat for terminal" = "Terminal için SimpleX Chat'i indir"; /* No comment provided by engineer. */ "Instant" = "Anında"; @@ -2994,7 +2968,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "geçersi̇z sohbet verisi"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "Geçersiz bağlanma bağlantısı"; /* invalid chat item */ @@ -3009,7 +2983,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Geçersiz taşıma onayı"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Geçersiz isim!"; /* No comment provided by engineer. */ @@ -3429,9 +3403,6 @@ snd error text */ /* No comment provided by engineer. */ "Migrate device" = "Cihazı taşıma"; -/* No comment provided by engineer. */ -"Migrate from another device" = "Başka bir cihazdan geçiş yapın"; - /* No comment provided by engineer. */ "Migrate here" = "Buraya göç edin"; @@ -3708,9 +3679,6 @@ snd error text */ /* No comment provided by engineer. */ "No unread chats" = "Okunmamış sohbet yok"; -/* No comment provided by engineer. */ -"No user identifiers." = "Herhangi bir kullanıcı tanımlayıcısı yok."; - /* No comment provided by engineer. */ "Not compatible!" = "Uyumlu değil!"; @@ -3767,7 +3735,7 @@ alert button new chat action */ "Ok" = "Tamam"; -/* No comment provided by engineer. */ +/* alert button */ "OK" = "TAMAM"; /* No comment provided by engineer. */ @@ -3848,7 +3816,8 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Sadece karşıdaki kişi sesli mesajlar gönderebilir."; -/* alert action */ +/* alert action +alert button */ "Open" = "Aç"; /* No comment provided by engineer. */ @@ -4103,12 +4072,6 @@ new chat action */ /* No comment provided by engineer. */ "Privacy policy and conditions of use." = "Gizlilik politikası ve kullanım koşulları."; -/* No comment provided by engineer. */ -"Privacy redefined" = "Gizlilik yeniden tanımlandı"; - -/* No comment provided by engineer. */ -"Private chats, groups and your contacts are not accessible to server operators." = "Özel sohbetler, gruplar ve kişilerinize sunucu operatörleri tarafından erişilemez."; - /* No comment provided by engineer. */ "Private filenames" = "Gizli dosya adları"; @@ -4148,9 +4111,6 @@ new chat action */ /* No comment provided by engineer. */ "Profile theme" = "Profil teması"; -/* alert message */ -"Profile update will be sent to your contacts." = "Profil güncellemesi kişilerinize gönderilecektir."; - /* No comment provided by engineer. */ "Prohibit audio/video calls." = "Sesli/görüntülü aramaları yasakla."; @@ -4239,16 +4199,10 @@ new chat action */ "Read more" = "Dahasını oku"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "[Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in our GitHub repository." = "GitHub deposunda daha fazlasını okuyun."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "[Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; - -/* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "[Kullanıcı Rehberi]nde daha fazlasını okuyun(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 deposu]nda daha fazlasını okuyun(https://github.com/simplex-chat/simplex-chat#readme)."; +"Read more in User Guide." = "Kullanıcı Rehberinde daha fazlasını okuyun."; /* No comment provided by engineer. */ "Receipts are disabled" = "Alındı onayları devre dışı bırakıldı"; @@ -4977,9 +4931,6 @@ chat item action */ /* No comment provided by engineer. */ "Share address publicly" = "Adresinizi herkese açık olarak paylaşın"; -/* alert title */ -"Share address with contacts?" = "Kişilerle adres paylaşılsın mı?"; - /* No comment provided by engineer. */ "Share from other apps." = "Diğer uygulamalardan paylaşın."; @@ -5004,9 +4955,6 @@ chat item action */ /* No comment provided by engineer. */ "Share to SimpleX" = "SimpleX ile paylaş"; -/* No comment provided by engineer. */ -"Share with contacts" = "Kişilerle paylaş"; - /* No comment provided by engineer. */ "Share your address" = "Adresini paylaş"; @@ -5109,9 +5057,6 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "SimpleX protokolleri Trail of Bits tarafından incelenmiştir."; -/* simplex link type */ -"SimpleX relay link" = "SimpleX aktarıcı bağlantısı"; - /* No comment provided by engineer. */ "Simplified incognito mode" = "Basitleştirilmiş gizli mod"; @@ -5164,6 +5109,9 @@ report reason */ /* chat item text */ "standard end-to-end encryption" = "standart uçtan uca şifreleme"; +/* No comment provided by engineer. */ +"Star on GitHub" = "Bize GitHub'da yıldız verin"; + /* No comment provided by engineer. */ "Start chat" = "Sohbeti başlat"; @@ -5269,9 +5217,6 @@ report reason */ /* No comment provided by engineer. */ "Tap Connect to use bot" = "Botu kullanmak için Bağlan tuşuna bas"; -/* No comment provided by engineer. */ -"Tap Create SimpleX address in the menu to create it later." = "Daha sonra oluşturmak için menüden BasitX adresi oluştur'a dokunun."; - /* No comment provided by engineer. */ "Tap Join group" = "Gruba katıl'a dokunun"; @@ -5317,7 +5262,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "Geçici dosya hatası"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Test %@ adımında başarısız oldu."; /* No comment provided by engineer. */ @@ -5374,9 +5320,6 @@ report reason */ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Şifreleme çalışıyor ve yeni şifreleme anlaşması gerekli değil. Bağlantı hatalarına neden olabilir!"; -/* No comment provided by engineer. */ -"The future of messaging" = "Gizli mesajlaşmanın yeni nesli"; - /* No comment provided by engineer. */ "The hash of the previous message is different." = "Önceki mesajın hash'i farklı."; @@ -5551,9 +5494,6 @@ report reason */ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Kişinizle uçtan uca şifrelemeyi doğrulamak için cihazlarınızdaki kodu karşılaştırın (veya tarayın)."; -/* No comment provided by engineer. */ -"Toggle chat list:" = "Sohbet listesini değiştir:"; - /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Bağlanırken gizli moda geçiş yap."; @@ -5671,7 +5611,7 @@ report reason */ /* swipe action */ "Unread" = "Okunmamış"; -/* No comment provided by engineer. */ +/* conn error description */ "Unsupported connection link" = "Desteklenmeyen bağlantı bağlantısı"; /* No comment provided by engineer. */ @@ -5746,9 +5686,6 @@ report reason */ /* No comment provided by engineer. */ "Use %@" = "%@ kullan"; -/* No comment provided by engineer. */ -"Use chat" = "Sohbeti kullan"; - /* new chat action */ "Use current profile" = "Şu anki profili kullan"; @@ -6157,9 +6094,6 @@ report reason */ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Doğrulanamadınız; lütfen tekrar deneyin."; -/* No comment provided by engineer. */ -"You decide who can connect." = "Kimin bağlanabileceğine siz karar verirsiniz."; - /* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Zaten bağlantı isteğinde bulundunuz!\nBağlantı isteği tekrarlansın mı?"; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 305e64fbcf..a0d9490b00 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -25,15 +25,9 @@ /* No comment provided by engineer. */ "(this device v%@)" = "(цей пристрій v%@)"; -/* 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. */ "**Create 1-time link**: to create and share a new invitation link." = "**Додати контакт**: створити нове посилання-запрошення."; @@ -398,9 +392,6 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "Активні з'єднання"; -/* 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 friends" = "Додайте друзів"; @@ -641,9 +632,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Відповісти на дзвінок"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "Кожен може хостити сервери."; - /* No comment provided by engineer. */ "App build: %@" = "Збірка програми: %@"; @@ -879,7 +867,7 @@ marked deleted chat item preview text */ /* 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. */ +/* chat link info line */ "Business address" = "Адреса підприємства"; /* No comment provided by engineer. */ @@ -894,9 +882,6 @@ marked deleted chat item preview text */ /* 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. */ -"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "Використовуючи SimpleX Chat, ви погоджуєтеся:\n- надсилати лише легальний контент у публічних групах.\n- поважати інших користувачів - без спаму."; - /* No comment provided by engineer. */ "call" = "дзвонити"; @@ -1080,7 +1065,8 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "Чат буде видалено для вас - цю дію неможливо скасувати!"; -/* chat toolbar */ +/* chat feature +chat toolbar */ "Chat with admins" = "Чат з адміністраторами"; /* No comment provided by engineer. */ @@ -1194,9 +1180,6 @@ set passcode view */ /* No comment provided by engineer. */ "Configure ICE servers" = "Налаштування серверів ICE"; -/* No comment provided by engineer. */ -"Configure server operators" = "Налаштувати операторів сервера"; - /* No comment provided by engineer. */ "Confirm" = "Підтвердити"; @@ -1230,7 +1213,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Підтверджений"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Підключіться"; /* No comment provided by engineer. */ @@ -1329,7 +1313,7 @@ set passcode view */ /* alert title */ "Connection error" = "Помилка підключення"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "Помилка підключення (AUTH)"; /* chat list item title (it should not be shown */ @@ -1428,6 +1412,9 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "Продовжуйте"; +/* No comment provided by engineer. */ +"Contribute" = "Внесок"; + /* No comment provided by engineer. */ "Conversation deleted!" = "Розмова видалена!"; @@ -1443,12 +1430,9 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Кут"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Виправити ім'я на %@?"; -/* No comment provided by engineer. */ -"Create" = "Створити"; - /* No comment provided by engineer. */ "Create 1-time link" = "Створити одноразове посилання"; @@ -1602,9 +1586,6 @@ set passcode view */ /* No comment provided by engineer. */ "Debug delivery" = "Доставка налагодження"; -/* No comment provided by engineer. */ -"Decentralized" = "Децентралізований"; - /* message decrypt error item */ "Decryption error" = "Помилка розшифровки"; @@ -1999,7 +1980,7 @@ chat item action */ /* No comment provided by engineer. */ "Empty message!" = "Порожнє повідомлення!"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "Увімкнути"; /* No comment provided by engineer. */ @@ -2029,9 +2010,6 @@ chat item action */ /* No comment provided by engineer. */ "Enable lock" = "Увімкнути блокування"; -/* No comment provided by engineer. */ -"Enable notifications" = "Увімкнути сповіщення"; - /* No comment provided by engineer. */ "Enable periodic notifications?" = "Увімкнути періодичні сповіщення?"; @@ -2173,7 +2151,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "помилка"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "Помилка"; /* No comment provided by engineer. */ @@ -2305,9 +2283,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Помилка відкриття чату"; -/* No comment provided by engineer. */ -"Error opening group" = "Помилка відкриття групи"; - /* alert title */ "Error receiving file" = "Помилка отримання файлу"; @@ -2550,7 +2525,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Швидше знаходьте чати"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Відбиток в адресі сервера не співпадає з сертифікатом."; /* No comment provided by engineer. */ @@ -2574,7 +2550,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Для всіх модераторів"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "Для профілю чату %@:"; /* No comment provided by engineer. */ @@ -2706,7 +2683,7 @@ snd error text */ /* No comment provided by engineer. */ "group is deleted" = "групу видалено"; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "Посилання на групу"; /* No comment provided by engineer. */ @@ -2832,9 +2809,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "Негайно"; -/* No comment provided by engineer. */ -"Immune to spam" = "Імунітет до спаму та зловживань"; - /* No comment provided by engineer. */ "Import" = "Імпорт"; @@ -2935,7 +2909,7 @@ snd error text */ "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)"; +"Install SimpleX Chat for terminal" = "Встановіть SimpleX Chat для терміналу"; /* No comment provided by engineer. */ "Instant" = "Миттєво"; @@ -2970,7 +2944,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "невірні дані чату"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "Неправильне посилання для підключення"; /* invalid chat item */ @@ -2985,7 +2959,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Недійсне підтвердження міграції"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Неправильне ім'я!"; /* No comment provided by engineer. */ @@ -3399,9 +3373,6 @@ snd error text */ /* No comment provided by engineer. */ "Migrate device" = "Перенести пристрій"; -/* No comment provided by engineer. */ -"Migrate from another device" = "Перехід з іншого пристрою"; - /* No comment provided by engineer. */ "Migrate here" = "Мігруйте сюди"; @@ -3678,9 +3649,6 @@ snd error text */ /* No comment provided by engineer. */ "No unread chats" = "Немає непрочитаних чатів"; -/* No comment provided by engineer. */ -"No user identifiers." = "Ніяких ідентифікаторів користувачів."; - /* No comment provided by engineer. */ "Not compatible!" = "Не сумісні!"; @@ -3737,7 +3705,7 @@ alert button new chat action */ "Ok" = "Гаразд"; -/* No comment provided by engineer. */ +/* alert button */ "OK" = "ОК"; /* No comment provided by engineer. */ @@ -3812,7 +3780,8 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Тільки ваш контакт може надсилати голосові повідомлення."; -/* alert action */ +/* alert action +alert button */ "Open" = "Відкрито"; /* No comment provided by engineer. */ @@ -4058,12 +4027,6 @@ new chat action */ /* No comment provided by engineer. */ "Privacy policy and conditions of use." = "Політика конфіденційності та умови використання."; -/* No comment provided by engineer. */ -"Privacy redefined" = "Конфіденційність переглянута"; - -/* No comment provided by engineer. */ -"Private chats, groups and your contacts are not accessible to server operators." = "Приватні чати, групи та ваші контакти недоступні для операторів сервера."; - /* No comment provided by engineer. */ "Private filenames" = "Приватні імена файлів"; @@ -4103,9 +4066,6 @@ new chat action */ /* No comment provided by engineer. */ "Profile theme" = "Тема профілю"; -/* alert message */ -"Profile update will be sent to your contacts." = "Оновлення профілю буде надіслано вашим контактам."; - /* No comment provided by engineer. */ "Prohibit audio/video calls." = "Заборонити аудіо/відеодзвінки."; @@ -4194,16 +4154,10 @@ new chat action */ "Read more" = "Читати далі"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Читайте більше в [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in our GitHub repository." = "Читайте більше в нашому GitHub репозиторії."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; - -/* 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)."; +"Read more in User Guide." = "Читайте більше в User Guide."; /* No comment provided by engineer. */ "Receipts are disabled" = "Підтвердження виключені"; @@ -4923,9 +4877,6 @@ chat item action */ /* No comment provided by engineer. */ "Share address publicly" = "Поділіться адресою публічно"; -/* alert title */ -"Share address with contacts?" = "Поділіться адресою з контактами?"; - /* No comment provided by engineer. */ "Share from other apps." = "Діліться з інших програм."; @@ -4950,9 +4901,6 @@ chat item action */ /* No comment provided by engineer. */ "Share to SimpleX" = "Поділіться з SimpleX"; -/* No comment provided by engineer. */ -"Share with contacts" = "Поділіться з контактами"; - /* No comment provided by engineer. */ "Share your address" = "Поділіться своєю адресою"; @@ -5107,6 +5055,9 @@ report reason */ /* chat item text */ "standard end-to-end encryption" = "стандартне наскрізне шифрування"; +/* No comment provided by engineer. */ +"Star on GitHub" = "Зірка на GitHub"; + /* No comment provided by engineer. */ "Start chat" = "Почати чат"; @@ -5209,9 +5160,6 @@ report reason */ /* No comment provided by engineer. */ "Tap Connect to send request" = "Натисніть Підключитися, щоб відправити запит"; -/* No comment provided by engineer. */ -"Tap Create SimpleX address in the menu to create it later." = "Натисніть «Створити адресу SimpleX» у меню, щоб створити її пізніше."; - /* No comment provided by engineer. */ "Tap Join group" = "Натисніть Приєднатися до групи"; @@ -5257,7 +5205,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "Тимчасова помилка файлу"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Тест завершився невдало на кроці %@."; /* No comment provided by engineer. */ @@ -5314,9 +5263,6 @@ report reason */ /* 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 future of messaging" = "Наступне покоління приватних повідомлень"; - /* No comment provided by engineer. */ "The hash of the previous message is different." = "Хеш попереднього повідомлення відрізняється."; @@ -5485,9 +5431,6 @@ report reason */ /* 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 chat list:" = "Перемикання списку чату:"; - /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Увімкніть інкогніто при підключенні."; @@ -5605,7 +5548,7 @@ report reason */ /* swipe action */ "Unread" = "Непрочитане"; -/* No comment provided by engineer. */ +/* conn error description */ "Unsupported connection link" = "Несумісне посилання для підключення"; /* No comment provided by engineer. */ @@ -5680,9 +5623,6 @@ report reason */ /* No comment provided by engineer. */ "Use %@" = "Використовуйте %@"; -/* No comment provided by engineer. */ -"Use chat" = "Використовуйте чат"; - /* new chat action */ "Use current profile" = "Використовувати поточний профіль"; @@ -6091,9 +6031,6 @@ report reason */ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Вас не вдалося верифікувати, спробуйте ще раз."; -/* No comment provided by engineer. */ -"You decide who can connect." = "Ви вирішуєте, хто може під'єднатися."; - /* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Ви вже надіслали запит на підключення!\nПовторити запит на підключення?"; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index d5afea745d..3893351fdd 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -25,15 +25,9 @@ /* No comment provided by engineer. */ "(this device v%@)" = "(此设备 v%@)"; -/* 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. */ "**Create 1-time link**: to create and share a new invitation link." = "**添加联系人**: 创建新的邀请链接,或通过您收到的链接进行连接."; @@ -395,9 +389,6 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "活动连接"; -/* 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 friends" = "添加好友"; @@ -647,9 +638,6 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "接听来电"; -/* No comment provided by engineer. */ -"Anybody can host servers." = "任何人都可以托管服务器。"; - /* No comment provided by engineer. */ "App build: %@" = "应用程序构建:%@"; @@ -791,6 +779,12 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "错误消息 ID"; +/* No comment provided by engineer. */ +"Be free in your network." = "在你的网络中自由畅行。"; + +/* No comment provided by engineer. */ +"Because we destroyed the power to know who you are. So that your power can never be taken." = "因为我们摧毁了知道你是谁的权力,因而您的权利永远不会被夺走。"; + /* No comment provided by engineer. */ "Better calls" = "更佳的通话"; @@ -894,7 +888,7 @@ marked deleted chat item preview text */ /* 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. */ +/* chat link info line */ "Business address" = "企业地址"; /* No comment provided by engineer. */ @@ -909,9 +903,6 @@ marked deleted chat item preview text */ /* 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. */ -"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "使用 SimpleX Chat 代表您同意:\n- 在公开群中只发送合法内容\n- 尊重其他用户 – 没有垃圾信息。"; - /* No comment provided by engineer. */ "call" = "呼叫"; @@ -1095,7 +1086,8 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "将为你删除聊天 - 此操作无法撤销!"; -/* chat toolbar */ +/* chat feature +chat toolbar */ "Chat with admins" = "和管理员聊天"; /* No comment provided by engineer. */ @@ -1209,9 +1201,6 @@ set passcode view */ /* No comment provided by engineer. */ "Configure ICE servers" = "配置 ICE 服务器"; -/* No comment provided by engineer. */ -"Configure server operators" = "配置服务器运营方"; - /* No comment provided by engineer. */ "Confirm" = "确认"; @@ -1245,7 +1234,8 @@ set passcode view */ /* token status text */ "Confirmed" = "已确定"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "连接"; /* No comment provided by engineer. */ @@ -1344,7 +1334,7 @@ set passcode view */ /* alert title */ "Connection error" = "连接错误"; -/* No comment provided by engineer. */ +/* conn error description */ "Connection error (AUTH)" = "连接错误(AUTH)"; /* chat list item title (it should not be shown */ @@ -1446,6 +1436,9 @@ set passcode view */ /* No comment provided by engineer. */ "Continue" = "继续"; +/* No comment provided by engineer. */ +"Contribute" = "贡献"; + /* No comment provided by engineer. */ "Conversation deleted!" = "对话已删除!"; @@ -1461,12 +1454,9 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "拐角"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "将名称更正为 %@?"; -/* No comment provided by engineer. */ -"Create" = "创建"; - /* No comment provided by engineer. */ "Create 1-time link" = "创建一次性链接"; @@ -1620,9 +1610,6 @@ set passcode view */ /* No comment provided by engineer. */ "Debug delivery" = "调试交付"; -/* No comment provided by engineer. */ -"Decentralized" = "分散式"; - /* message decrypt error item */ "Decryption error" = "解密错误"; @@ -2023,7 +2010,7 @@ chat item action */ /* No comment provided by engineer. */ "Empty message!" = "空消息!"; -/* No comment provided by engineer. */ +/* alert button */ "Enable" = "启用"; /* No comment provided by engineer. */ @@ -2053,9 +2040,6 @@ chat item action */ /* No comment provided by engineer. */ "Enable lock" = "启用锁定"; -/* No comment provided by engineer. */ -"Enable notifications" = "启用通知"; - /* No comment provided by engineer. */ "Enable periodic notifications?" = "启用定期通知?"; @@ -2197,7 +2181,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "错误"; -/* No comment provided by engineer. */ +/* conn error description */ "Error" = "错误"; /* No comment provided by engineer. */ @@ -2329,9 +2313,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "打开聊天时出错"; -/* No comment provided by engineer. */ -"Error opening group" = "打开群时出错"; - /* alert title */ "Error receiving file" = "接收文件错误"; @@ -2445,7 +2426,8 @@ file error text snd error text */ "Error: %@" = "错误: %@"; -/* server test error */ +/* relay test error +server test error */ "Error: %@." = "错误:%@。"; /* No comment provided by engineer. */ @@ -2595,7 +2577,8 @@ snd error text */ /* No comment provided by engineer. */ "Fingerprint in server address does not match certificate: %@." = "服务器的指纹与证书不符:%@。"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "服务器地址中的证书指纹可能不正确"; /* No comment provided by engineer. */ @@ -2619,7 +2602,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "所有 moderators"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "为聊天资料 %@:"; /* No comment provided by engineer. */ @@ -2751,7 +2735,7 @@ snd error text */ /* No comment provided by engineer. */ "group is deleted" = "群被删除了"; -/* No comment provided by engineer. */ +/* chat link info line */ "Group link" = "群组链接"; /* No comment provided by engineer. */ @@ -2880,9 +2864,6 @@ snd error text */ /* No comment provided by engineer. */ "Immediately" = "立即"; -/* No comment provided by engineer. */ -"Immune to spam" = "不受垃圾和骚扰消息影响"; - /* No comment provided by engineer. */ "Import" = "导入"; @@ -2983,7 +2964,7 @@ snd error text */ "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)"; +"Install SimpleX Chat for terminal" = "安装用于终端的 SimpleX Chat"; /* No comment provided by engineer. */ "Instant" = "即时"; @@ -3018,7 +2999,7 @@ snd error text */ /* No comment provided by engineer. */ "invalid chat data" = "无效聊天数据"; -/* No comment provided by engineer. */ +/* conn error description */ "Invalid connection link" = "无效的连接链接"; /* invalid chat item */ @@ -3033,7 +3014,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "迁移确认无效"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "无效名称!"; /* No comment provided by engineer. */ @@ -3459,9 +3440,6 @@ snd error text */ /* No comment provided by engineer. */ "Migrate device" = "迁移设备"; -/* No comment provided by engineer. */ -"Migrate from another device" = "从另一台设备迁移"; - /* No comment provided by engineer. */ "Migrate here" = "迁移到此处"; @@ -3742,7 +3720,10 @@ snd error text */ "No unread chats" = "没有未读聊天"; /* No comment provided by engineer. */ -"No user identifiers." = "没有用户标识符。"; +"Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "没有人追踪你的谈话内容。没有人绘制你去过的地方的地图。隐私从来都不是一项功能--而是一种生活方式。"; + +/* No comment provided by engineer. */ +"Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "别人家的门锁再好也比不上这里。房东再好也比不上这里,他既尊重你的隐私,又保留着所有访客的记录。你不是客人,你是家。没有国王能闯入--你是主人。"; /* No comment provided by engineer. */ "Not compatible!" = "不兼容!"; @@ -3800,7 +3781,7 @@ alert button new chat action */ "Ok" = "好的"; -/* No comment provided by engineer. */ +/* alert button */ "OK" = "好的"; /* No comment provided by engineer. */ @@ -3881,7 +3862,8 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "只有您的联系人可以发送语音消息。"; -/* alert action */ +/* alert action +alert button */ "Open" = "打开"; /* No comment provided by engineer. */ @@ -4133,12 +4115,6 @@ new chat action */ /* No comment provided by engineer. */ "Privacy policy and conditions of use." = "隐私政策和使用条款。"; -/* No comment provided by engineer. */ -"Privacy redefined" = "重新定义隐私"; - -/* No comment provided by engineer. */ -"Private chats, groups and your contacts are not accessible to server operators." = "服务器运营方无法访问私密聊天、群组和你的联系人。"; - /* No comment provided by engineer. */ "Private filenames" = "私密文件名"; @@ -4178,9 +4154,6 @@ new chat action */ /* No comment provided by engineer. */ "Profile theme" = "个人资料主题"; -/* alert message */ -"Profile update will be sent to your contacts." = "个人资料更新将被发送给您的联系人。"; - /* No comment provided by engineer. */ "Prohibit audio/video calls." = "禁止音频/视频通话。"; @@ -4269,16 +4242,10 @@ new chat action */ "Read more" = "阅读更多"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "阅读更多[User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)。"; +"Read more in our GitHub repository." = "在我们的 GitHub 仓库 中阅读更多信息。"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "在 [用户指南](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) 中阅读更多内容。"; - -/* 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) 中阅读更多信息。"; +"Read more in User Guide." = "阅读更多User Guide。"; /* No comment provided by engineer. */ "Receipts are disabled" = "回执已禁用"; @@ -5019,9 +4986,6 @@ chat item action */ /* No comment provided by engineer. */ "Share address publicly" = "公开分享地址"; -/* alert title */ -"Share address with contacts?" = "与联系人分享地址?"; - /* No comment provided by engineer. */ "Share from other apps." = "从其他应用程序共享。"; @@ -5046,9 +5010,6 @@ chat item action */ /* No comment provided by engineer. */ "Share to SimpleX" = "分享到 SimpleX"; -/* No comment provided by engineer. */ -"Share with contacts" = "与联系人分享"; - /* No comment provided by engineer. */ "Share your address" = "分享地址"; @@ -5151,9 +5112,6 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "SimpleX 协议由 Trail of Bits 审阅。"; -/* simplex link type */ -"SimpleX relay link" = "SimpleX 中继链接"; - /* No comment provided by engineer. */ "Simplified incognito mode" = "简化的隐身模式"; @@ -5206,6 +5164,9 @@ report reason */ /* chat item text */ "standard end-to-end encryption" = "标准端到端加密"; +/* No comment provided by engineer. */ +"Star on GitHub" = "在 GitHub 上加星"; + /* No comment provided by engineer. */ "Start chat" = "开始聊天"; @@ -5308,9 +5269,6 @@ report reason */ /* No comment provided by engineer. */ "Tap Connect to use bot" = "轻按“连接”使用机器人"; -/* No comment provided by engineer. */ -"Tap Create SimpleX address in the menu to create it later." = "要稍后创建 SimpleX 地址,请在菜单中轻按“创建 SimpleX 地址”"; - /* No comment provided by engineer. */ "Tap Join group" = "轻按加入群"; @@ -5356,7 +5314,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "临时文件错误"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "在步骤 %@ 上测试失败。"; /* No comment provided by engineer. */ @@ -5413,9 +5372,6 @@ report reason */ /* 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 future of messaging" = "下一代私密通讯软件"; - /* No comment provided by engineer. */ "The hash of the previous message is different." = "上一条消息的散列不同。"; @@ -5440,6 +5396,9 @@ report reason */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "旧数据库在迁移过程中没有被移除,可以删除。"; +/* No comment provided by engineer. */ +"The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it." = "人类最古老的自由--与他人交谈而不被监视--建立在不会背叛它的基础设施之上。"; + /* No comment provided by engineer. */ "The second preset operator in the app!" = "应用中的第二个预设运营方!"; @@ -5461,6 +5420,12 @@ report reason */ /* No comment provided by engineer. */ "Themes" = "主题"; +/* No comment provided by engineer. */ +"Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible." = "然后我们转向线上,每个平台都要求你提供一些信息--你的姓名、电话号码、好友列表。我们接受了这样一个事实:与人交流的代价就是让别人知道我们在和谁交流。每一代人,每一代科技,都遵循着这样的模式--电话、电子邮件、即时通讯、社交媒体。这似乎是唯一可行的方式。"; + +/* No comment provided by engineer. */ +"There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected." = "还有另一种方法。一个没有电话号码、没有用户名、没有账户、没有任何用户身份的网络。一个连接人们并传输加密信息的网络,而无需知道谁连接了。"; + /* No comment provided by engineer. */ "These conditions will also apply for: **%@**." = "这些条件将同样适用于: **%@**。"; @@ -5584,9 +5549,6 @@ report reason */ /* 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 chat list:" = "切换聊天列表:"; - /* No comment provided by engineer. */ "Toggle incognito when connecting." = "在连接时切换隐身模式。"; @@ -5704,7 +5666,7 @@ report reason */ /* swipe action */ "Unread" = "未读"; -/* No comment provided by engineer. */ +/* conn error description */ "Unsupported connection link" = "不支持的连接链接"; /* No comment provided by engineer. */ @@ -5779,9 +5741,6 @@ report reason */ /* No comment provided by engineer. */ "Use %@" = "使用 %@"; -/* No comment provided by engineer. */ -"Use chat" = "使用聊天"; - /* new chat action */ "Use current profile" = "使用当前配置文件"; @@ -6199,9 +6158,6 @@ report reason */ /* No comment provided by engineer. */ "You could not be verified; please try again." = "您的身份无法验证,请再试一次。"; -/* No comment provided by engineer. */ -"You decide who can connect." = "你决定谁可以连接。"; - /* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "您已经请求连接了!\n重复连接请求?"; @@ -6253,6 +6209,9 @@ report reason */ /* snd group event chat item */ "you unblocked %@" = "您解封了 %@"; +/* No comment provided by engineer. */ +"You were born without an account" = "你生来就没有账户。"; + /* No comment provided by engineer. */ "You will be able to send messages **only after your request is accepted**." = "**只有在你的请求被接受后**你才能发送消息。"; @@ -6322,6 +6281,9 @@ report reason */ /* No comment provided by engineer. */ "Your contacts will remain connected." = "与您的联系人保持连接。"; +/* No comment provided by engineer. */ +"Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public." = "你的对话内容始终属于你,就像互联网出现之前一样。网络不是一个你访问的地方,而是一个你创建并拥有的地方。无论你将其设为私密还是公开,任何人都无法将其夺走。"; + /* No comment provided by engineer. */ "Your credentials may be sent unencrypted." = "你的凭据可能以未经加密的方式被发送。"; diff --git a/apps/multiplatform/.gitignore b/apps/multiplatform/.gitignore index 5d39eb29f2..bc00225c87 100644 --- a/apps/multiplatform/.gitignore +++ b/apps/multiplatform/.gitignore @@ -16,4 +16,7 @@ android/build android/release common/build desktop/build -release \ No newline at end of file +release + +# Generated SimpleX assets +common/src/commonMain/resources/assets/simplex/ \ No newline at end of file diff --git a/apps/multiplatform/android/src/main/AndroidManifest.xml b/apps/multiplatform/android/src/main/AndroidManifest.xml index d6059896a5..9e059afa14 100644 --- a/apps/multiplatform/android/src/main/AndroidManifest.xml +++ b/apps/multiplatform/android/src/main/AndroidManifest.xml @@ -51,6 +51,7 @@ ("copySimplexAssets") { + dependsOn(verifySimplexAssets) + from(srcImagesDir) + into(simplexAssetsLocal.resolve("MR/images")) + } +} else { + tasks.register("cleanSimplexAssets") { + delete(simplexAssetsLocal) + } +} + kotlin { androidTarget() jvm("desktop") @@ -31,6 +56,11 @@ kotlin { } val commonMain by getting { + if (hasSimplexAssets) { + resources.srcDir(simplexAssetsLocal) + } else { + resources.srcDir("src/commonMain/resources/assets/default") + } dependencies { api(compose.runtime) api(compose.foundation) @@ -118,8 +148,8 @@ kotlin { implementation("org.slf4j:slf4j-simple:2.0.12") implementation("uk.co.caprica:vlcj:4.8.3") implementation("net.java.dev.jna:jna:5.14.0") - implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a") - implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a") + implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf") + implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf") implementation("com.squareup.okhttp3:okhttp:4.12.0") } } @@ -160,12 +190,18 @@ buildConfig { buildConfigField("int", "DESKTOP_VERSION_CODE", "${extra["desktop.version_code"]}") buildConfigField("String", "DATABASE_BACKEND", "\"${extra["database.backend"]}\"") buildConfigField("Boolean", "ANDROID_BUNDLE", "${extra["android.bundle"]}") + buildConfigField("Boolean", "SIMPLEX_ASSETS", "$hasSimplexAssets") } } afterEvaluate { tasks.named("generateMRcommonMain") { dependsOn("adjustFormatting") + if (hasSimplexAssets) { + dependsOn("copySimplexAssets") + } else { + dependsOn("cleanSimplexAssets") + } } tasks.create("adjustFormatting") { doLast { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index 56279a5143..011619bab0 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -394,7 +394,7 @@ private fun ActiveCallOverlayLayout( DisabledBackgroundCallsButton() } - BoxWithConstraints(Modifier.padding(start = 6.dp, end = 6.dp, bottom = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) { + BoxWithConstraints(Modifier.navigationBarsPadding().padding(start = 6.dp, end = 6.dp, bottom = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) { val size = ((maxWidth - DEFAULT_PADDING_HALF * 4) / 5).coerceIn(0.dp, 60.dp) // limiting max width for tablets/wide screens, will be displayed in the center val padding = ((min(420.dp, maxWidth) - size * 5) / 4).coerceAtLeast(0.dp) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt index d9d3af7bb7..a4fc74f6d4 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt @@ -10,7 +10,7 @@ import chat.simplex.res.MR @Composable actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference, onclick: (() -> Unit)?) { if (user == null) { - OnboardingActionButton(Modifier.fillMaxWidth(), labelId = MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onclick = onclick) + OnboardingActionButton(Modifier.fillMaxWidth(), labelId = MR.strings.get_started, onboarding = OnboardingStage.Step2_CreateProfile, onclick = onclick) } else { OnboardingActionButton(Modifier.fillMaxWidth(), labelId = MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onclick = onclick) } 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 index d9439a5474..7542a0b8c6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -34,6 +34,7 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.ModalManager.Companion.fromEndToStartTransition import chat.simplex.common.views.helpers.ModalManager.Companion.fromStartToEndTransition import chat.simplex.common.views.localauth.VerticalDivider +import chat.simplex.common.views.newchat.* import chat.simplex.common.views.onboarding.* import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR @@ -141,10 +142,8 @@ fun MainScreen() { when { onboarding == OnboardingStage.Step1_SimpleXInfo && chatModel.migrationState.value != null -> { // In migration process. Nothing should interrupt it, that's why it's the first branch in when() - SimpleXInfo(chatModel, onboarding = true) - if (appPlatform.isDesktop) { - ModalManager.fullscreen.showInView() - } + if (appPlatform.isDesktop) DesktopOnboarding(onboarding, chatModel) + else SimpleXInfo(chatModel, onboarding = true) } chatModel.dbMigrationInProgress.value -> DefaultProgressView(stringResource(MR.strings.database_migration_in_progress)) chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database)) @@ -174,36 +173,31 @@ fun MainScreen() { } } } - else -> AnimatedContent(targetState = onboarding, - transitionSpec = { - if (targetState > initialState) { - fromEndToStartTransition() - } else { - fromStartToEndTransition() - }.using(SizeTransform(clip = false)) - } - ) { state -> - when (state) { - OnboardingStage.OnboardingComplete -> { /* handled out of AnimatedContent block */} - OnboardingStage.Step1_SimpleXInfo -> { - SimpleXInfo(chatModel, onboarding = true) - if (appPlatform.isDesktop) { - ModalManager.fullscreen.showInView() + else -> { + if (appPlatform.isDesktop) { + DesktopOnboarding(onboarding, chatModel) + } else { + AnimatedContent(targetState = onboarding, + transitionSpec = { + if (targetState > initialState) { + fromEndToStartTransition() + } else { + fromStartToEndTransition() + }.using(SizeTransform(clip = false)) + } + ) { state -> + when (state) { + OnboardingStage.OnboardingComplete -> {} + OnboardingStage.Step1_SimpleXInfo -> SimpleXInfo(chatModel, onboarding = true) + OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {} + OnboardingStage.LinkAMobile -> LinkAMobile() + OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) + OnboardingStage.Step3_ChooseServerOperators, + OnboardingStage.Step3_CreateSimpleXAddress, + OnboardingStage.Step4_SetNotificationsMode -> YourNetworkView(chatModel) + OnboardingStage.Step4_NetworkCommitments -> OnboardingConditionsView(chatModel) } } - OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {} - OnboardingStage.LinkAMobile -> LinkAMobile() - OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) - OnboardingStage.Step3_ChooseServerOperators -> { - val modalData = remember { ModalData() } - modalData.OnboardingConditionsView() - if (appPlatform.isDesktop) { - ModalManager.fullscreen.showInView() - } - } - // Ensure backwards compatibility with old onboarding stage for address creation, otherwise notification setup would be skipped - OnboardingStage.Step3_CreateSimpleXAddress -> SetNotificationsMode(chatModel) - OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) } } } @@ -275,6 +269,27 @@ fun MainScreen() { } } +@Composable +private fun DesktopOnboarding(onboarding: OnboardingStage, chatModel: ChatModel) { + if (onboarding == OnboardingStage.LinkAMobile) { + LinkAMobile() + ModalManager.fullscreen.showInView() + } else { + DesktopOnboardingShell(onboarding) { + when (onboarding) { + OnboardingStage.Step1_SimpleXInfo -> SimpleXInfo(chatModel, onboarding = true) + OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {} + OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) + OnboardingStage.Step3_ChooseServerOperators, + OnboardingStage.Step3_CreateSimpleXAddress, + OnboardingStage.Step4_SetNotificationsMode -> YourNetworkView(chatModel) + OnboardingStage.Step4_NetworkCommitments -> OnboardingConditionsView(chatModel) + else -> {} + } + } + } +} + val ANDROID_CALL_TOP_PADDING = 40.dp @Composable @@ -383,7 +398,9 @@ fun CenterPartOfScreen() { } when (currentChatId.value) { null -> { - if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) { + if (shouldShowOnboarding()) { + ConnectOnboardingView() + } else if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) { Box( Modifier .fillMaxSize() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 0ac7a1b973..80b68f37a5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -92,6 +92,7 @@ object ChannelRelaysModel { if (groupId.value == groupInfo.groupId) { val i = groupRelays.indexOfFirst { it.groupRelayId == relay.groupRelayId } if (i >= 0) groupRelays[i] = relay + else groupRelays.add(relay) } } @@ -1157,10 +1158,10 @@ object ChatModel { showingInvitation.value = null chatsContext.chatItems.clearAndNotify() chatModel.chatId.value = withId + ModalManager.start.closeModals() + ModalManager.end.closeModals() } } - ModalManager.start.closeModals() - ModalManager.end.closeModals() } } @@ -1596,8 +1597,7 @@ sealed class ChatInfo: SomeChat, NamedChat { } } - val userCantSendReason: Pair? - get() { + fun userCantSendReason(allRelaysBroken: Boolean = false): Pair? { when (this) { is Direct -> { if (contact.sendMsgToConnect) return null @@ -1618,6 +1618,9 @@ sealed class ChatInfo: SomeChat, NamedChat { if (groupInfo.membership.memberActive) { when (groupChatScope) { null -> { + if (allRelaysBroken && groupInfo.useRelays) { + return generalGetString(MR.strings.cant_broadcast_message) to null + } if (groupInfo.membership.memberPending) { return generalGetString(MR.strings.reviewed_by_admins) to generalGetString(MR.strings.observer_cant_send_message_desc) } @@ -1666,7 +1669,7 @@ sealed class ChatInfo: SomeChat, NamedChat { } } - val sendMsgEnabled get() = userCantSendReason == null + val sendMsgEnabled get() = userCantSendReason() == null val sndReady: Boolean get() = when(this) { @@ -1685,6 +1688,18 @@ sealed class ChatInfo: SomeChat, NamedChat { else -> null } + val sendAsGroup: Boolean get() { + val g = (this as? Group)?.groupInfo + return if (g != null && g.useRelays && g.membership.memberRole >= GroupMemberRole.Owner) { + when (groupChatScope()) { + null -> true + is GroupChatScope.MemberSupport -> false + } + } else { + false + } + } + fun ntfsEnabled(ci: ChatItem): Boolean = ntfsEnabled(ci.meta.userMention) @@ -1748,6 +1763,9 @@ sealed class ChatInfo: SomeChat, NamedChat { is Group -> groupInfo else -> null } + + val isChannel: Boolean + get() = groupInfo_?.useRelays == true } @Serializable @@ -2089,6 +2107,7 @@ data class GroupInfo ( ChatFeature.Calls -> false } override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null } + val isChannel: Boolean get() = groupProfile.isChannel override val displayName get() = localAlias.ifEmpty { groupProfile.displayName } override val fullName get() = groupProfile.fullName override val shortDescr get() = groupProfile.shortDescr @@ -2108,7 +2127,7 @@ data class GroupInfo ( val chatIconName: ImageResource get() = if (useRelays) { - MR.images.ic_bigtop_updates_padded + MR.images.ic_bigtop_updates_circle_filled } else when (businessChat?.chatType) { null -> MR.images.ic_supervised_user_circle_filled BusinessChatType.Business -> MR.images.ic_work_filled_padded @@ -2127,6 +2146,7 @@ data class GroupInfo ( GroupFeature.SimplexLinks -> p.simplexLinks.on(membership) GroupFeature.Reports -> p.reports.on GroupFeature.History -> p.history.on + GroupFeature.Support -> p.support.on } } @@ -2208,6 +2228,8 @@ data class GroupProfile ( val groupPreferences: GroupPreferences? = null, val memberAdmission: GroupMemberAdmission? = null ): NamedChat { + val isChannel: Boolean get() = publicGroup?.groupType == GroupType.Channel + companion object { val sampleData = GroupProfile( displayName = "team", @@ -2267,13 +2289,15 @@ enum class RelayStatus { @SerialName("new") RsNew, @SerialName("invited") RsInvited, @SerialName("accepted") RsAccepted, - @SerialName("active") RsActive; + @SerialName("active") RsActive, + @SerialName("inactive") RsInactive; val text: String get() = when (this) { RsNew -> generalGetString(MR.strings.relay_status_new) RsInvited -> generalGetString(MR.strings.relay_status_invited) RsAccepted -> generalGetString(MR.strings.relay_status_accepted) RsActive -> generalGetString(MR.strings.relay_status_active) + RsInactive -> generalGetString(MR.strings.relay_status_inactive) } } @@ -2891,12 +2915,14 @@ data class ChatItem ( val id: Long get() = meta.itemId val timestampText: String get() = meta.timestampText - val text: String get() { + val text: String get() = text(isChannel = false) + + fun text(isChannel: Boolean): String { val mc = content.msgContent return when { - content.text == "" && file != null && mc is MsgContent.MCVoice -> String.format(generalGetString(MR.strings.voice_message_with_duration), durationText(mc.duration)) - content.text == "" && file != null -> file.fileName - else -> content.text + content.text(isChannel) == "" && file != null && mc is MsgContent.MCVoice -> String.format(generalGetString(MR.strings.voice_message_with_duration), durationText(mc.duration)) + content.text(isChannel) == "" && file != null -> file.fileName + else -> content.text(isChannel) } } @@ -3035,6 +3061,7 @@ data class ChatItem ( is CIContent.RcvCall -> false // notification is shown on CallInvitation instead is CIContent.RcvIntegrityError -> false is CIContent.RcvDecryptionError -> false + is CIContent.RcvMsgErrorContent -> false is CIContent.RcvGroupInvitation -> true is CIContent.SndGroupInvitation -> false is CIContent.RcvDirectEventContent -> when (content.rcvDirectEvent) { @@ -3729,6 +3756,7 @@ sealed class CIContent: ItemContent { @Serializable @SerialName("rcvCall") class RcvCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvIntegrityError") class RcvIntegrityError(val msgError: MsgErrorType): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvDecryptionError") class RcvDecryptionError(val msgDecryptError: MsgDecryptError, val msgCount: UInt): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("rcvMsgError") class RcvMsgErrorContent(val rcvMsgError: RcvMsgError): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvGroupInvitation") class RcvGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvDirectEvent") class RcvDirectEventContent(val rcvDirectEvent: RcvDirectEvent): CIContent() { override val msgContent: MsgContent? get() = null } @@ -3754,7 +3782,9 @@ sealed class CIContent: ItemContent { @Serializable @SerialName("chatBanner") object ChatBanner: CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("invalidJSON") data class InvalidJSON(val json: String): CIContent() { override val msgContent: MsgContent? get() = null } - override val text: String get() = when (this) { + override val text: String get() = text(isChannel = false) + + fun text(isChannel: Boolean): String = when (this) { is SndMsgContent -> msgContent.text is RcvMsgContent -> msgContent.text is SndDeleted -> generalGetString(MR.strings.deleted_description) @@ -3763,11 +3793,12 @@ sealed class CIContent: ItemContent { is RcvCall -> status.text(duration) is RcvIntegrityError -> msgError.text is RcvDecryptionError -> msgDecryptError.text + is RcvMsgErrorContent -> rcvMsgError.text is RcvGroupInvitation -> groupInvitation.text is SndGroupInvitation -> groupInvitation.text is RcvDirectEventContent -> rcvDirectEvent.text - is RcvGroupEventContent -> rcvGroupEvent.text - is SndGroupEventContent -> sndGroupEvent.text + is RcvGroupEventContent -> rcvGroupEvent.text(isChannel) + is SndGroupEventContent -> sndGroupEvent.text(isChannel) is RcvConnEventContent -> rcvConnEvent.text is SndConnEventContent -> sndConnEvent.text is RcvChatFeature -> featureText(feature, enabled.text, param) @@ -3783,8 +3814,8 @@ sealed class CIContent: ItemContent { is RcvBlocked -> generalGetString(MR.strings.blocked_by_admin_item_description) is SndDirectE2EEInfo -> directE2EEInfoStr(e2eeInfo) is RcvDirectE2EEInfo -> directE2EEInfoStr(e2eeInfo) - is SndGroupE2EEInfo -> e2eeInfoNoPQStr - is RcvGroupE2EEInfo -> e2eeInfoNoPQStr + is SndGroupE2EEInfo -> groupE2EEInfoStr(e2eeInfo) + is RcvGroupE2EEInfo -> groupE2EEInfoStr(e2eeInfo) is ChatBanner -> "" is InvalidJSON -> "invalid data" } @@ -3803,6 +3834,7 @@ sealed class CIContent: ItemContent { is RcvCall -> true is RcvIntegrityError -> true is RcvDecryptionError -> true + is RcvMsgErrorContent -> true is RcvGroupInvitation -> true is RcvModerated -> true is RcvBlocked -> true @@ -3820,6 +3852,9 @@ sealed class CIContent: ItemContent { private val e2eeInfoNoPQStr: String = generalGetString(MR.strings.e2ee_info_no_pq_short) + fun groupE2EEInfoStr(e2EEInfo: E2EEInfo): String = + if (e2EEInfo.public == true) generalGetString(MR.strings.e2ee_info_no_e2ee) else e2eeInfoNoPQStr + fun featureText(feature: Feature, enabled: String, param: Int?, role: GroupMemberRole? = null): String = (if (feature.hasParam) { "${feature.text}: ${timeText(param)}" @@ -4292,7 +4327,7 @@ sealed class MsgContent { @Serializable(with = MsgContentSerializer::class) class MCVoice(override val text: String, val duration: Int): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCReport(override val text: String, val reason: ReportReason): MsgContent() - @Serializable(with = MsgContentSerializer::class) class MCChat(override val text: String, val chatLink: MsgChatLink): MsgContent() + @Serializable(with = MsgContentSerializer::class) class MCChat(override val text: String, val chatLink: MsgChatLink, val ownerSig: LinkOwnerSig? = null): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent() val isVoice: Boolean get() = @@ -4346,7 +4381,7 @@ enum class CIGroupInvitationStatus { } @Serializable -class E2EEInfo (val pqEnabled: Boolean?) {} +class E2EEInfo (val pqEnabled: Boolean?, val public: Boolean? = null) {} object MsgContentSerializer : KSerializer { override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgContent", PolymorphicKind.SEALED) { @@ -4413,7 +4448,8 @@ object MsgContentSerializer : KSerializer { } "chat" -> { val chatLink = decoder.json.decodeFromString(json["chatLink"].toString()) - MsgContent.MCChat(text, chatLink) + val ownerSig = json["ownerSig"]?.let { decoder.json.decodeFromJsonElement(it) } + MsgContent.MCChat(text, chatLink, ownerSig) } else -> MsgContent.MCUnknown(t, text, json) } @@ -4474,6 +4510,7 @@ object MsgContentSerializer : KSerializer { put("type", "chat") put("text", value.text) put("chatLink", json.encodeToJsonElement(value.chatLink)) + value.ownerSig?.let { put("ownerSig", json.encodeToJsonElement(it)) } } is MsgContent.MCUnknown -> value.json } @@ -4481,16 +4518,51 @@ object MsgContentSerializer : KSerializer { } } -@Serializable -enum class MsgContentTag { - @SerialName("text") Text, - @SerialName("link") Link, - @SerialName("image") Image, - @SerialName("video") Video, - @SerialName("voice") Voice, - @SerialName("file") File, - @SerialName("report") Report, - @SerialName("chat") Chat, +@Serializable(with = MsgContentTagSerializer::class) +sealed class MsgContentTag { + @Serializable @SerialName("text") object Text: MsgContentTag() + @Serializable @SerialName("link") object Link: MsgContentTag() + @Serializable @SerialName("image") object Image: MsgContentTag() + @Serializable @SerialName("video") object Video: MsgContentTag() + @Serializable @SerialName("voice") object Voice: MsgContentTag() + @Serializable @SerialName("file") object File: MsgContentTag() + @Serializable @SerialName("report") object Report: MsgContentTag() + @Serializable @SerialName("chat") object Chat: MsgContentTag() + @Serializable @SerialName("unknown") data class Unknown(val type: String): MsgContentTag() + + val cmdString: String get() = when (this) { + is Text -> "text" + is Link -> "link" + is Image -> "image" + is Video -> "video" + is Voice -> "voice" + is File -> "file" + is Report -> "report" + is Chat -> "chat" + is Unknown -> type + } +} + +object MsgContentTagSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("MsgContentTag", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): MsgContentTag = + when (val s = decoder.decodeString()) { + "text" -> MsgContentTag.Text + "link" -> MsgContentTag.Link + "image" -> MsgContentTag.Image + "video" -> MsgContentTag.Video + "voice" -> MsgContentTag.Voice + "file" -> MsgContentTag.File + "report" -> MsgContentTag.Report + "chat" -> MsgContentTag.Chat + "liveText" -> MsgContentTag.Text + else -> MsgContentTag.Unknown(s) + } + + override fun serialize(encoder: Encoder, value: MsgContentTag) { + encoder.encodeString(value.cmdString) + } } @Serializable @@ -4498,8 +4570,82 @@ sealed class MsgChatLink { @Serializable @SerialName("contact") data class Contact(val connLink: String, val profile: Profile, val business: Boolean) : MsgChatLink() @Serializable @SerialName("invitation") data class Invitation(val invLink: String, val profile: Profile) : MsgChatLink() @Serializable @SerialName("group") data class Group(val connLink: String, val groupProfile: GroupProfile) : MsgChatLink() + + val isPublicGroup: Boolean + get() = (this as? Group)?.groupProfile?.publicGroup != null + + val connLinkStr: String + get() = when (this) { + is Group -> connLink + is Contact -> connLink + is Invitation -> invLink + } + + val image: String? + get() = when (this) { + is Group -> groupProfile.image + is Contact -> profile.image + is Invitation -> profile.image + } + + val displayName: String + get() = when (this) { + is Group -> groupProfile.displayName + is Contact -> profile.displayName + is Invitation -> profile.displayName + } + + val fullName: String + get() = when (this) { + is Group -> groupProfile.fullName + is Contact -> profile.fullName + is Invitation -> profile.fullName + } + + val shortDescription: String? + get() { + val s = when (this) { + is Group -> groupProfile.shortDescr + is Contact -> profile.shortDescr + is Invitation -> profile.shortDescr + } + return s?.trim()?.ifEmpty { null } + } + + val iconRes: ImageResource + get() = when (this) { + is Group -> if (groupProfile.isChannel) MR.images.ic_bigtop_updates_circle_filled else MR.images.ic_supervised_user_circle_filled + is Contact -> if (business) MR.images.ic_work_filled_padded else MR.images.ic_account_circle_filled + is Invitation -> MR.images.ic_account_circle_filled + } + + val smallIconRes: ImageResource + get() = when (this) { + is Group -> if (groupProfile.isChannel) MR.images.ic_bigtop_updates else MR.images.ic_group + is Contact -> if (business) MR.images.ic_work else MR.images.ic_person + is Invitation -> MR.images.ic_person + } + + fun infoLine(signed: Boolean): String { + var s = when (this) { + is Group -> if (groupProfile.isChannel) generalGetString(MR.strings.chat_link_channel) else generalGetString(MR.strings.chat_link_group) + is Contact -> if (business) generalGetString(MR.strings.chat_link_business_address) else generalGetString(MR.strings.chat_link_contact_address) + is Invitation -> generalGetString(MR.strings.chat_link_one_time) + } + if (signed) { + s += " " + if (isPublicGroup) generalGetString(MR.strings.chat_link_from_owner) else generalGetString(MR.strings.chat_link_signed) + } + return s + } } +@Serializable +data class LinkOwnerSig( + val ownerId: String? = null, + val chatBinding: String, + val ownerSig: String +) + @Serializable class FormattedText(val text: String, val format: Format? = null) { val linkUri: String? get() = @@ -4714,6 +4860,17 @@ sealed class MsgErrorType() { } } +@Serializable +sealed class RcvMsgError() { + @Serializable @SerialName("dropped") class Dropped(val attempts: Int): RcvMsgError() + @Serializable @SerialName("parseError") class ParseError(val parseError: String): RcvMsgError() + + val text: String get() = when (this) { + is Dropped -> String.format(generalGetString(MR.strings.rcv_msg_error_dropped), attempts) + is ParseError -> String.format(generalGetString(MR.strings.rcv_msg_error_parse), parseError) + } +} + @Serializable sealed class RcvDirectEvent() { @Serializable @SerialName("contactDeleted") class ContactDeleted(): RcvDirectEvent() @@ -4764,7 +4921,9 @@ sealed class RcvGroupEvent() { @Serializable @SerialName("memberProfileUpdated") class MemberProfileUpdated(val fromProfile: Profile, val toProfile: Profile): RcvGroupEvent() @Serializable @SerialName("newMemberPendingReview") class NewMemberPendingReview(): RcvGroupEvent() - val text: String get() = when (this) { + val text: String get() = text(isChannel = false) + + fun text(isChannel: Boolean): String = when (this) { is MemberAdded -> String.format(generalGetString(MR.strings.rcv_group_event_member_added), profile.profileViewName) is MemberConnected -> generalGetString(MR.strings.rcv_group_event_member_connected) is MemberAccepted -> String.format(generalGetString(MR.strings.rcv_group_event_member_accepted), profile.profileViewName) @@ -4779,8 +4938,8 @@ sealed class RcvGroupEvent() { is UserRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_your_role), role.text) is MemberDeleted -> String.format(generalGetString(MR.strings.rcv_group_event_member_deleted), profile.profileViewName) is UserDeleted -> generalGetString(MR.strings.rcv_group_event_user_deleted) - is GroupDeleted -> generalGetString(MR.strings.rcv_group_event_group_deleted) - is GroupUpdated -> generalGetString(MR.strings.rcv_group_event_updated_group_profile) + is GroupDeleted -> generalGetString(if (isChannel) MR.strings.rcv_channel_event_channel_deleted else MR.strings.rcv_group_event_group_deleted) + is GroupUpdated -> generalGetString(if (isChannel) MR.strings.rcv_channel_event_updated_channel_profile else 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) is MemberProfileUpdated -> profileUpdatedText(fromProfile, toProfile) @@ -4812,7 +4971,9 @@ sealed class SndGroupEvent() { @Serializable @SerialName("memberAccepted") class MemberAccepted(val groupMemberId: Long, val profile: Profile): SndGroupEvent() @Serializable @SerialName("userPendingReview") class UserPendingReview(): SndGroupEvent() - val text: String get() = when (this) { + val text: String get() = text(isChannel = false) + + fun text(isChannel: Boolean): String = when (this) { is MemberRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_member_role), profile.profileViewName, role.text) is UserRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_role_for_yourself), role.text) is MemberBlocked -> if (blocked) { @@ -4822,7 +4983,7 @@ sealed class SndGroupEvent() { } is MemberDeleted -> String.format(generalGetString(MR.strings.snd_group_event_member_deleted), profile.profileViewName) is UserLeft -> generalGetString(MR.strings.snd_group_event_user_left) - is GroupUpdated -> generalGetString(MR.strings.snd_group_event_group_profile_updated) + is GroupUpdated -> generalGetString(if (isChannel) MR.strings.snd_channel_event_channel_profile_updated else MR.strings.snd_group_event_group_profile_updated) is MemberAccepted -> generalGetString(MR.strings.snd_group_event_member_accepted) is UserPendingReview -> generalGetString(MR.strings.snd_group_event_user_pending_review) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index cb42ee2aba..88b4e387df 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -58,6 +58,7 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* +import java.util.concurrent.atomic.AtomicBoolean import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.Date @@ -212,7 +213,7 @@ class AppPreferences { val shouldImportAppSettings = mkBoolPreference(SHARED_PREFS_SHOULD_IMPORT_APP_SETTINGS, false) val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM_THEME_NAME) - val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.themeName) + val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.DARK.themeName) val currentThemeIds = mkMapPreference(SHARED_PREFS_CURRENT_THEME_IDs, mapOf(), encode = { json.encodeToString(MapSerializer(String.serializer(), String.serializer()), it) }, decode = { @@ -266,6 +267,7 @@ class AppPreferences { showReportsInSupportChatAlert to true, showDeleteConversationNotice to true, showDeleteContactNotice to true, + privacyLinkPreviewsShowAlert to true, ) private fun mkIntPreference(prefName: String, default: Int) = @@ -722,14 +724,20 @@ object ChatController { val alert = if (r is API.Error) retryableNetworkErrorAlert(r.err) else null if ((inProgress == null || inProgress.value) && alert != null) { return suspendCancellableCoroutine { cont -> + val resumed = AtomicBoolean(false) + fun safeResume(result: Result) { + if (resumed.compareAndSet(false, true)) { + cont.resumeWith(result) + } + } showRetryAlert( alert, onCancel = { - cont.resumeWith(Result.success(null)) + safeResume(Result.success(null)) }, onRetry = { withLongRunningApi { - cont.resumeWith( + safeResume( runCatching { coroutineScope { sendCmdWithRetry(rhId, cmd, inProgress = inProgress, retryNum = retryNum + 1) @@ -741,7 +749,7 @@ object ChatController { ) cont.invokeOnCancellation { - cont.resumeWith(Result.success(null)) + safeResume(Result.success(null)) } } } else { @@ -1043,7 +1051,7 @@ object ChatController { suspend fun apiGetChatContentTypes(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?): List? { val r = sendCmd(rh, CC.ApiGetChatContentTypes(type, id, scope)) - if (r is API.Result && r.res is CR.ChatContentTypes) return r.res.contentTypes + if (r is API.Result && r.res is CR.ChatContentTypes) return r.res.contentTypes.filter { it !is MsgContentTag.Unknown } Log.e(TAG, "apiGetChatContentTypes bad response: ${r.responseType} ${r.details}") AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_loading_details), "${r.responseType}: ${r.details}") return null @@ -1135,6 +1143,13 @@ object ChatController { return processSendMessageCmd(rh, cmd)?.map { it.chatItem } } + suspend fun apiShareChatMsgContent(rh: Long?, shareChatType: ChatType, shareChatId: Long, toChatType: ChatType, toChatId: Long, toScope: GroupChatScope?, sendAsGroup: Boolean): MsgContent? { + val r = sendCmd(rh, CC.ApiShareChatMsgContent(shareChatType, shareChatId, toChatType, toChatId, toScope, sendAsGroup)) + if (r is API.Result && r.res is CR.ChatMsgContent) return r.res.msgContent + apiErrorAlert("apiShareChatMsgContent", generalGetString(MR.strings.error_sharing_channel), r) + return null + } + suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, chatItemIds: List): CR.ForwardPlan? { val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, fromScope, chatItemIds)) if (r is API.Result && r.res is CR.ForwardPlan) return r.res @@ -1485,9 +1500,9 @@ object ChatController { return null } - suspend fun apiConnectPlan(rh: Long?, connLink: String, inProgress: MutableState): Pair? { + suspend fun apiConnectPlan(rh: Long?, connLink: String, linkOwnerSig: LinkOwnerSig? = null, inProgress: MutableState): Pair? { val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null } - val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink), inProgress = inProgress) + val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink, linkOwnerSig), inProgress = inProgress) if (r is API.Result && r.res is CR.CRConnectionPlan) return r.res.connLink to r.res.connectionPlan if (inProgress.value && r != null) apiConnectResponseAlert(r) return null @@ -1557,6 +1572,23 @@ object ChatController { } } + fun connErrorText(e: ChatError): String = when { + e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.InvalidConnReq -> + generalGetString(MR.strings.invalid_connection_link) + e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.UnsupportedConnReq -> + generalGetString(MR.strings.unsupported_connection_link) + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH -> + generalGetString(MR.strings.connection_error_auth) + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.BLOCKED -> + "${generalGetString(MR.strings.connection_error_blocked)}: ${e.agentError.smpErr.blockInfo.reason.text}" + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.QUOTA -> + generalGetString(MR.strings.connection_reached_limit_of_undelivered_messages) + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.BROKER -> + generalGetString(MR.strings.network_error) + else -> + "${generalGetString(MR.strings.error_prefix)}: ${e.string}" + } + suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData): Chat? { val userId = try { currentUserId("apiPrepareContact") } catch (e: Exception) { return null } val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData)) @@ -2111,10 +2143,16 @@ object ChatController { return null } - suspend fun apiNewPublicGroup(rh: Long?, incognito: Boolean, relayIds: List, groupProfile: GroupProfile): Triple>? { + sealed class PublicGroupCreationResult { + data class Created(val groupInfo: GroupInfo, val groupLink: GroupLink, val groupRelays: List): PublicGroupCreationResult() + data class CreationFailed(val addRelayResults: List): PublicGroupCreationResult() + } + + suspend fun apiNewPublicGroup(rh: Long?, incognito: Boolean, relayIds: List, groupProfile: GroupProfile): PublicGroupCreationResult? { val userId = kotlin.runCatching { currentUserId("apiNewPublicGroup") }.getOrElse { return null } val r = sendCmdWithRetry(rh, CC.ApiNewPublicGroup(userId, incognito, relayIds, groupProfile)) - if (r is API.Result && r.res is CR.PublicGroupCreated) return Triple(r.res.groupInfo, r.res.groupLink, r.res.groupRelays) + if (r is API.Result && r.res is CR.PublicGroupCreated) return PublicGroupCreationResult.Created(r.res.groupInfo, r.res.groupLink, r.res.groupRelays) + if (r is API.Result && r.res is CR.PublicGroupCreationFailed) return PublicGroupCreationResult.CreationFailed(r.res.addRelayResults) if (r != null) throw Exception("${r.responseType}: ${r.details}") return null } @@ -2125,6 +2163,19 @@ object ChatController { return emptyList() } + sealed class AddGroupRelaysResult { + data class Added(val groupInfo: GroupInfo, val groupLink: GroupLink, val groupRelays: List): AddGroupRelaysResult() + data class AddFailed(val addRelayResults: List): AddGroupRelaysResult() + } + + suspend fun apiAddGroupRelays(groupId: Long, relayIds: List): AddGroupRelaysResult? { + val r = sendCmdWithRetry(null, CC.ApiAddGroupRelays(groupId, relayIds)) + if (r is API.Result && r.res is CR.GroupRelaysAdded) return AddGroupRelaysResult.Added(r.res.groupInfo, r.res.groupLink, r.res.groupRelays) + if (r is API.Result && r.res is CR.GroupRelaysAddFailed) return AddGroupRelaysResult.AddFailed(r.res.addRelayResults) + if (r != null) throw Exception("${r.responseType}: ${r.details}") + return null + } + suspend fun apiAddMember(rh: Long?, groupId: Long, contactId: Long, memberRole: GroupMemberRole): GroupMember? { val r = sendCmd(rh, CC.ApiAddMember(groupId, contactId, memberRole)) if (r is API.Result && r.res is CR.SentGroupInvitation) return r.res.member @@ -2217,18 +2268,19 @@ object ChatController { return emptyList() } - suspend fun apiUpdateGroup(rh: Long?, groupId: Long, groupProfile: GroupProfile): GroupInfo? { + suspend fun apiUpdateGroup(rh: Long?, groupId: Long, groupProfile: GroupProfile, isChannel: Boolean): GroupInfo? { val r = sendCmd(rh, CC.ApiUpdateGroupProfile(groupId, groupProfile)) + val errorTitle = if (isChannel) MR.strings.error_saving_channel_profile else MR.strings.error_saving_group_profile return when { r is API.Result && r.res is CR.GroupUpdated -> r.res.toGroup r is API.Error -> { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_group_profile), "$r.err") + AlertManager.shared.showAlertMsg(generalGetString(errorTitle), "$r.err") null } else -> { Log.e(TAG, "apiUpdateGroup bad response: ${r.responseType} ${r.details}") AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.error_saving_group_profile), + generalGetString(errorTitle), "${r.responseType}: ${r.details}" ) null @@ -3623,9 +3675,11 @@ sealed class CC { class ApiGetReactionMembers(val userId: Long, val groupId: Long, val itemId: Long, val reaction: MsgReaction): CC() class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val chatItemIds: List): CC() class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean, val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val itemIds: List, val ttl: Int?): CC() + class ApiShareChatMsgContent(val shareChatType: ChatType, val shareChatId: Long, val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean): CC() class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC() class ApiNewPublicGroup(val userId: Long, val incognito: Boolean, val relayIds: List, val groupProfile: GroupProfile): CC() class ApiGetGroupRelays(val groupId: Long): CC() + class ApiAddGroupRelays(val groupId: Long, val relayIds: List): CC() class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC() class ApiJoinGroup(val groupId: Long): CC() class ApiAcceptMember(val groupId: Long, val groupMemberId: Long, val memberRole: GroupMemberRole): CC() @@ -3682,7 +3736,7 @@ sealed class CC { class APIAddContact(val userId: Long, val incognito: Boolean): CC() class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC() - class APIConnectPlan(val userId: Long, val connLink: String): CC() + class APIConnectPlan(val userId: Long, val connLink: String, val linkOwnerSig: LinkOwnerSig? = null): CC() class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData): CC() class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val directLink: Boolean, val groupShortLinkData: GroupShortLinkData): CC() class APIChangePreparedContactUser(val contactId: Long, val newUserId: Long): CC() @@ -3789,7 +3843,7 @@ sealed class CC { val tag = if (contentTag == null) { "" } else { - " content=${contentTag.name.lowercase()}" + " content=${contentTag.cmdString}" } "/_get chat ${chatRef(type, id, scope)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search") } @@ -3821,12 +3875,16 @@ sealed class CC { val ttlStr = if (ttl != null) "$ttl" else "default" "/_forward ${chatRef(toChatType, toChatId, toScope)}${if (sendAsGroup) " as_group=on" else ""} ${chatRef(fromChatType, fromChatId, fromScope)} ${itemIds.joinToString(",")} ttl=${ttlStr}" } + is ApiShareChatMsgContent -> { + "/_share chat content ${chatRef(shareChatType, shareChatId, null)} ${chatRef(toChatType, toChatId, toScope)}${if (sendAsGroup) "(as_group=on)" else ""}" + } is ApiPlanForwardChatItems -> { "/_forward plan ${chatRef(fromChatType, fromChatId, fromScope)} ${chatItemIds.joinToString(",")}" } is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}" is ApiNewPublicGroup -> "/_public group $userId incognito=${onOff(incognito)} ${relayIds.joinToString(",")} ${json.encodeToString(groupProfile)}" is ApiGetGroupRelays -> "/_get relays #$groupId" + is ApiAddGroupRelays -> "/_add relays #$groupId ${relayIds.joinToString(",")}" is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}" is ApiJoinGroup -> "/_join #$groupId" is ApiAcceptMember -> "/_accept member #$groupId $groupMemberId ${memberRole.memberRole}" @@ -3883,7 +3941,10 @@ sealed class CC { is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}" is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}" is ApiChangeConnectionUser -> "/_set conn user :$connId $userId" - is APIConnectPlan -> "/_connect plan $userId $connLink" + is APIConnectPlan -> { + val sigStr = if (linkOwnerSig != null) " sig=${json.encodeToString(linkOwnerSig)}" else "" + "/_connect plan $userId $connLink$sigStr" + } is APIPrepareContact -> "/_prepare contact $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(contactShortLinkData)}" is APIPrepareGroup -> "/_prepare group $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} direct=${onOff(directLink)} ${json.encodeToString(groupShortLinkData)}" is APIChangePreparedContactUser -> "/_set contact user @$contactId $newUserId" @@ -4002,10 +4063,12 @@ sealed class CC { is ApiChatItemReaction -> "apiChatItemReaction" is ApiGetReactionMembers -> "apiGetReactionMembers" is ApiForwardChatItems -> "apiForwardChatItems" + is ApiShareChatMsgContent -> "apiShareChatMsgContent" is ApiPlanForwardChatItems -> "apiPlanForwardChatItems" is ApiNewGroup -> "apiNewGroup" is ApiNewPublicGroup -> "apiNewPublicGroup" is ApiGetGroupRelays -> "apiGetGroupRelays" + is ApiAddGroupRelays -> "apiAddGroupRelays" is ApiAddMember -> "apiAddMember" is ApiJoinGroup -> "apiJoinGroup" is ApiAcceptMember -> "apiAcceptMember" @@ -4553,6 +4616,12 @@ data class RelayConnectionResult( val relayError: ChatError? = null ) +@Serializable +data class AddRelayResult( + val relay: UserChatRelay, + val relayError: ChatError? = null +) + @Serializable data class GroupShortLinkInfo( val direct: Boolean, @@ -5640,7 +5709,8 @@ enum class GroupFeature: Feature { @SerialName("files") Files, @SerialName("simplexLinks") SimplexLinks, @SerialName("reports") Reports, - @SerialName("history") History; + @SerialName("history") History, + @SerialName("support") Support; override val hasParam: Boolean get() = when(this) { TimedMessages -> true @@ -5658,10 +5728,12 @@ enum class GroupFeature: Feature { SimplexLinks -> true Reports -> false History -> false + Support -> false } - override val text: String - get() = when(this) { + override val text: String get() = text(isChannel = false) + + fun text(isChannel: Boolean): String = when(this) { TimedMessages -> generalGetString(MR.strings.timed_messages) DirectMessages -> generalGetString(MR.strings.direct_messages) FullDelete -> generalGetString(MR.strings.full_deletion) @@ -5669,8 +5741,9 @@ enum class GroupFeature: Feature { Voice -> generalGetString(MR.strings.voice_messages) Files -> generalGetString(MR.strings.files_and_media) SimplexLinks -> generalGetString(MR.strings.simplex_links) - Reports -> generalGetString(MR.strings.group_reports_member_reports) + Reports -> generalGetString(if (isChannel) MR.strings.group_reports_subscriber_reports else MR.strings.group_reports_member_reports) History -> generalGetString(MR.strings.recent_history) + Support -> generalGetString(MR.strings.chat_with_admins) } val icon: Painter @@ -5684,6 +5757,7 @@ enum class GroupFeature: Feature { SimplexLinks -> painterResource(MR.images.ic_link) Reports -> painterResource(MR.images.ic_flag) History -> painterResource(MR.images.ic_schedule) + Support -> painterResource(MR.images.ic_help) } @Composable @@ -5697,9 +5771,10 @@ enum class GroupFeature: Feature { SimplexLinks -> painterResource(MR.images.ic_link) Reports -> painterResource(MR.images.ic_flag_filled) History -> painterResource(MR.images.ic_schedule_filled) + Support -> painterResource(MR.images.ic_help_filled) } - fun enableDescription(enabled: GroupFeatureEnabled, canEdit: Boolean): String = + fun enableDescription(enabled: GroupFeatureEnabled, canEdit: Boolean, isChannel: Boolean = false): String = if (canEdit) { when(this) { TimedMessages -> when(enabled) { @@ -5707,8 +5782,8 @@ enum class GroupFeature: Feature { GroupFeatureEnabled.OFF -> generalGetString(MR.strings.prohibit_sending_disappearing) } DirectMessages -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(MR.strings.allow_direct_messages) - GroupFeatureEnabled.OFF -> generalGetString(MR.strings.prohibit_direct_messages) + GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.allow_direct_messages_channel else MR.strings.allow_direct_messages) + GroupFeatureEnabled.OFF -> generalGetString(if (isChannel) MR.strings.prohibit_direct_messages_channel else MR.strings.prohibit_direct_messages) } FullDelete -> when(enabled) { GroupFeatureEnabled.ON -> generalGetString(MR.strings.allow_to_delete_messages) @@ -5735,47 +5810,55 @@ enum class GroupFeature: Feature { GroupFeatureEnabled.OFF -> generalGetString(MR.strings.disable_sending_member_reports) } History -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(MR.strings.enable_sending_recent_history) - GroupFeatureEnabled.OFF -> generalGetString(MR.strings.disable_sending_recent_history) + GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.enable_sending_recent_history_channel else MR.strings.enable_sending_recent_history) + GroupFeatureEnabled.OFF -> generalGetString(if (isChannel) MR.strings.disable_sending_recent_history_channel else MR.strings.disable_sending_recent_history) + } + Support -> when(enabled) { + GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.allow_chat_with_admins_channel else MR.strings.allow_chat_with_admins) + GroupFeatureEnabled.OFF -> generalGetString(MR.strings.prohibit_chat_with_admins) } } } else { when(this) { TimedMessages -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_send_disappearing) + GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.group_members_can_send_disappearing_channel else MR.strings.group_members_can_send_disappearing) GroupFeatureEnabled.OFF -> generalGetString(MR.strings.disappearing_messages_are_prohibited) } DirectMessages -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_send_dms) - GroupFeatureEnabled.OFF -> generalGetString(MR.strings.direct_messages_are_prohibited) + GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.group_members_can_send_dms_channel else MR.strings.group_members_can_send_dms) + GroupFeatureEnabled.OFF -> generalGetString(if (isChannel) MR.strings.direct_messages_are_prohibited_channel else MR.strings.direct_messages_are_prohibited) } FullDelete -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_delete) + GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.group_members_can_delete_channel else MR.strings.group_members_can_delete) GroupFeatureEnabled.OFF -> generalGetString(MR.strings.message_deletion_prohibited_in_chat) } Reactions -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_add_message_reactions) + GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.group_members_can_add_message_reactions_channel else MR.strings.group_members_can_add_message_reactions) GroupFeatureEnabled.OFF -> generalGetString(MR.strings.message_reactions_are_prohibited) } Voice -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_send_voice) + GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.group_members_can_send_voice_channel else MR.strings.group_members_can_send_voice) GroupFeatureEnabled.OFF -> generalGetString(MR.strings.voice_messages_are_prohibited) } Files -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_send_files) + GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.group_members_can_send_files_channel else MR.strings.group_members_can_send_files) GroupFeatureEnabled.OFF -> generalGetString(MR.strings.files_are_prohibited_in_group) } SimplexLinks -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_send_simplex_links) + GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.group_members_can_send_simplex_links_channel else MR.strings.group_members_can_send_simplex_links) GroupFeatureEnabled.OFF -> generalGetString(MR.strings.simplex_links_are_prohibited_in_group) } Reports -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_send_reports) + GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.group_members_can_send_reports_channel else MR.strings.group_members_can_send_reports) GroupFeatureEnabled.OFF -> generalGetString(MR.strings.member_reports_are_prohibited) } History -> when(enabled) { - GroupFeatureEnabled.ON -> generalGetString(MR.strings.recent_history_is_sent_to_new_members) - GroupFeatureEnabled.OFF -> generalGetString(MR.strings.recent_history_is_not_sent_to_new_members) + GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.recent_history_is_sent_to_new_members_channel else MR.strings.recent_history_is_sent_to_new_members) + GroupFeatureEnabled.OFF -> generalGetString(if (isChannel) MR.strings.recent_history_is_not_sent_to_new_members_channel else MR.strings.recent_history_is_not_sent_to_new_members) + } + Support -> when(enabled) { + GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.members_can_chat_with_admins_channel else MR.strings.members_can_chat_with_admins) + GroupFeatureEnabled.OFF -> generalGetString(MR.strings.chat_with_admins_is_prohibited) } } } @@ -5902,6 +5985,7 @@ data class FullGroupPreferences( val simplexLinks: RoleGroupPreference, val reports: GroupPreference, val history: GroupPreference, + val support: GroupPreference, val commands: List, ) { fun toGroupPreferences(): GroupPreferences = @@ -5915,6 +5999,7 @@ data class FullGroupPreferences( simplexLinks = simplexLinks, reports = reports, history = history, + support = support, commands = commands, ) @@ -5929,6 +6014,7 @@ data class FullGroupPreferences( simplexLinks = RoleGroupPreference(GroupFeatureEnabled.ON, role = null), reports = GroupPreference(GroupFeatureEnabled.ON), history = GroupPreference(GroupFeatureEnabled.ON), + support = GroupPreference(GroupFeatureEnabled.ON), commands = listOf() ) } @@ -5945,6 +6031,7 @@ data class GroupPreferences( val simplexLinks: RoleGroupPreference? = null, val reports: GroupPreference? = null, val history: GroupPreference? = null, + val support: GroupPreference? = null, val commands: List? = null ) { companion object { @@ -6317,6 +6404,7 @@ sealed class CR { @Serializable @SerialName("subscriptionStatus") class SubscriptionStatusEvt(val subscriptionStatus: SubscriptionStatus, val connections: List): CR() @Serializable @SerialName("chatInfoUpdated") class ChatInfoUpdated(val user: UserRef, val chatInfo: ChatInfo): CR() @Serializable @SerialName("newChatItems") class NewChatItems(val user: UserRef, val chatItems: List): CR() + @Serializable @SerialName("chatMsgContent") class ChatMsgContent(val user: UserRef, val msgContent: MsgContent): CR() @Serializable @SerialName("chatItemsStatusesUpdated") class ChatItemsStatusesUpdated(val user: UserRef, val chatItems: List): 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() @@ -6328,7 +6416,10 @@ sealed class CR { // group events @Serializable @SerialName("groupCreated") class GroupCreated(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("publicGroupCreated") class PublicGroupCreated(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink, val groupRelays: List): CR() + @Serializable @SerialName("publicGroupCreationFailed") class PublicGroupCreationFailed(val user: UserRef, val addRelayResults: List): CR() @Serializable @SerialName("groupRelays") class GroupRelays(val user: UserRef, val groupInfo: GroupInfo, val groupRelays: List): CR() + @Serializable @SerialName("groupRelaysAdded") class GroupRelaysAdded(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink, val groupRelays: List): CR() + @Serializable @SerialName("groupRelaysAddFailed") class GroupRelaysAddFailed(val user: UserRef, val addRelayResults: List): 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("groupLinkConnecting") class GroupLinkConnecting (val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember): CR() @@ -6505,6 +6596,7 @@ sealed class CR { is SubscriptionStatusEvt -> "subscriptionStatus" is ChatInfoUpdated -> "chatInfoUpdated" is NewChatItems -> "newChatItems" + is ChatMsgContent -> "chatMsgContent" is ChatItemsStatusesUpdated -> "chatItemsStatusesUpdated" is ChatItemUpdated -> "chatItemUpdated" is ChatItemNotChanged -> "chatItemNotChanged" @@ -6515,7 +6607,10 @@ sealed class CR { is ForwardPlan -> "forwardPlan" is GroupCreated -> "groupCreated" is PublicGroupCreated -> "publicGroupCreated" + is PublicGroupCreationFailed -> "publicGroupCreationFailed" is GroupRelays -> "groupRelays" + is GroupRelaysAdded -> "groupRelaysAdded" + is GroupRelaysAddFailed -> "groupRelaysAddFailed" is SentGroupInvitation -> "sentGroupInvitation" is UserAcceptedGroupSent -> "userAcceptedGroupSent" is GroupLinkConnecting -> "groupLinkConnecting" @@ -6685,6 +6780,7 @@ sealed class CR { is SubscriptionStatusEvt -> "subscriptionStatus $subscriptionStatus\nconnections: $connections" is ChatInfoUpdated -> withUser(user, json.encodeToString(chatInfo)) is NewChatItems -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) }) + is ChatMsgContent -> withUser(user, msgContent.toString()) is ChatItemsStatusesUpdated -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) }) is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem)) is ChatItemNotChanged -> withUser(user, json.encodeToString(chatItem)) @@ -6695,7 +6791,10 @@ sealed class CR { is ForwardPlan -> withUser(user, "itemsCount: $itemsCount\nchatItemIds: ${json.encodeToString(chatItemIds)}\nforwardConfirmation: ${json.encodeToString(forwardConfirmation)}") is GroupCreated -> withUser(user, json.encodeToString(groupInfo)) is PublicGroupCreated -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink\ngroupRelays: $groupRelays") + is PublicGroupCreationFailed -> withUser(user, "addRelayResults: $addRelayResults") is GroupRelays -> withUser(user, "groupInfo: $groupInfo\ngroupRelays: $groupRelays") + is GroupRelaysAdded -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink\ngroupRelays: $groupRelays") + is GroupRelaysAddFailed -> withUser(user, "addRelayResults: $addRelayResults") is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member") is UserAcceptedGroupSent -> json.encodeToString(groupInfo) is GroupLinkConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember") @@ -6839,6 +6938,12 @@ fun simplexChatLink(uri: String): String = if (uri.startsWith("simplex:/")) uri.replace("simplex:/", "https://simplex.chat/") else uri +@Serializable +sealed class OwnerVerification { + @Serializable @SerialName("verified") object Verified : OwnerVerification() + @Serializable @SerialName("failed") class Failed(val reason: String) : OwnerVerification() +} + @Serializable sealed class ConnectionPlan { @Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() @@ -6849,7 +6954,7 @@ sealed class ConnectionPlan { @Serializable sealed class InvitationLinkPlan { - @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null): InvitationLinkPlan() + @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null, val ownerVerification: OwnerVerification? = null): InvitationLinkPlan() @Serializable @SerialName("ownLink") object OwnLink: InvitationLinkPlan() @Serializable @SerialName("connecting") class Connecting(val contact_: Contact? = null): InvitationLinkPlan() @Serializable @SerialName("known") class Known(val contact: Contact): InvitationLinkPlan() @@ -6857,7 +6962,7 @@ sealed class InvitationLinkPlan { @Serializable sealed class ContactAddressPlan { - @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null): ContactAddressPlan() + @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null, val ownerVerification: OwnerVerification? = null): ContactAddressPlan() @Serializable @SerialName("ownLink") object OwnLink: ContactAddressPlan() @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: ContactAddressPlan() @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val contact: Contact): ContactAddressPlan() @@ -6867,11 +6972,12 @@ sealed class ContactAddressPlan { @Serializable sealed class GroupLinkPlan { - @Serializable @SerialName("ok") class Ok(val groupSLinkInfo_: GroupShortLinkInfo? = null, val groupSLinkData_: GroupShortLinkData? = null): GroupLinkPlan() + @Serializable @SerialName("ok") class Ok(val groupSLinkInfo_: GroupShortLinkInfo? = null, val groupSLinkData_: GroupShortLinkData? = null, val ownerVerification: OwnerVerification? = null): GroupLinkPlan() @Serializable @SerialName("ownLink") class OwnLink(val groupInfo: GroupInfo): GroupLinkPlan() @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: GroupLinkPlan() @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan() @Serializable @SerialName("known") class Known(val groupInfo: GroupInfo): GroupLinkPlan() + @Serializable @SerialName("noRelays") class NoRelays(val groupSLinkData_: GroupShortLinkData? = null): GroupLinkPlan() } abstract class TerminalItem { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 36a7ae1a80..3805a8e8b7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -162,11 +162,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat } else if (startChat().await()) { val savedOnboardingStage = appPreferences.onboardingStage.get() val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { - if (appPlatform.isAndroid) { - OnboardingStage.Step4_SetNotificationsMode - } else { - OnboardingStage.OnboardingComplete - } + OnboardingStage.Step4_NetworkCommitments } else { savedOnboardingStage } 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 index 88d9fbb705..cdd4140e3f 100644 --- 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 @@ -42,7 +42,9 @@ expect fun desktopOpenDir(dir: File) fun createURIFromPath(absolutePath: String): URI = URI.create(URLEncoder.encode(absolutePath, "UTF-8")) -fun URI.toFile(): File = File(URLDecoder.decode(rawPath, "UTF-8").removePrefix("file:")) +fun URI.toFile(): File = + if (scheme == "file") File(this) + else File(URLDecoder.decode(rawPath, "UTF-8").removePrefix("file:")) fun copyFileToFile(from: File, to: URI, finally: () -> Unit) { try { 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 index 39fcea3981..385120f18b 100644 --- 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 @@ -44,7 +44,7 @@ abstract class NtfManager { chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId) ) { - displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem)) + displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem, cInfo.isChannel)) } } @@ -119,7 +119,7 @@ abstract class NtfManager { } } - private fun hideSecrets(cItem: ChatItem): String { + private fun hideSecrets(cItem: ChatItem, isChannel: Boolean = false): String { val md = cItem.formattedText return if (md != null) { var res = "" @@ -130,9 +130,9 @@ abstract class NtfManager { } else { val mc = cItem.content.msgContent if (mc is MsgContent.MCReport) { - generalGetString(MR.strings.notification_group_report).format(cItem.text.ifEmpty { mc.reason.text }) + generalGetString(MR.strings.notification_group_report).format(cItem.text(isChannel).ifEmpty { mc.reason.text }) } else { - cItem.text + cItem.text(isChannel) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt index c50ea5c349..3df780ae24 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt @@ -3,16 +3,16 @@ package chat.simplex.common.ui.theme import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.ui.graphics.* -import chat.simplex.common.views.helpers.mixWith -import kotlin.math.min +import androidx.compose.ui.graphics.colorspace.ColorSpaces +import kotlin.math.cos +import kotlin.math.sin + +fun oklch(L: Float, C: Float, H: Float, alpha: Float = 1f): Color { + val hRad = H * (Math.PI.toFloat() / 180f) + return Color(L, C * cos(hRad), C * sin(hRad), alpha, ColorSpaces.Oklab) +} -val Purple200 = Color(0xFFBB86FC) -val Purple500 = Color(0xFF6200EE) -val Purple700 = Color(0xFF3700B3) -val Teal200 = Color(0xFF03DAC5) -val Gray = Color(0x22222222) val Indigo = Color(0xFF9966FF) val SimplexBlue = Color(0, 136, 255, 255) // If this value changes also need to update #0088ff in string resource files val SimplexGreen = Color(77, 218, 103, 255) @@ -29,8 +29,8 @@ val GroupDark = Color(80, 80, 80, 60) val IncomingCallLight = Color(239, 237, 236, 255) val WarningOrange = Color(255, 127, 0, 255) val WarningYellow = Color(255, 192, 0, 255) -val FileLight = Color(183, 190, 199, 255) -val FileDark = Color(101, 101, 106, 255) +val FileLight = Color(191, 194, 199, 255) +val FileDark = Color(94, 94, 98, 255) val MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black val NoteFolderIconColor: Color @Composable get() = MaterialTheme.appColors.primaryVariant2 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt index 1b5a81a819..1de47df7ce 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt @@ -626,6 +626,7 @@ val DEFAULT_BOTTOM_BUTTON_PADDING = 20.dp val DEFAULT_MIN_SECTION_ITEM_HEIGHT = 50.dp val DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL = 15.dp +val DEFAULT_WINDOW_WIDTH = 1366.dp val DEFAULT_START_MODAL_WIDTH = 388.dp val DEFAULT_MIN_CENTER_MODAL_WIDTH = 590.dp val DEFAULT_END_MODAL_WIDTH = 388.dp diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Type.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Type.kt index 9acfffb3ac..9b0f89c36d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Type.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Type.kt @@ -10,7 +10,7 @@ val Typography = Typography( h1 = TextStyle( fontFamily = Inter, fontWeight = FontWeight.Bold, - fontSize = 32.sp, + fontSize = 33.5.sp, ), h2 = TextStyle( fontFamily = Inter, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index 6ec124048c..3e4b8ce1db 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -4,6 +4,7 @@ import SectionTextFooter import androidx.compose.foundation.* 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.material.* import androidx.compose.material.MaterialTheme.colors @@ -11,28 +12,44 @@ 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.clip +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.* +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +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.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import chat.simplex.common.BuildConfigCommon import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.migration.MigrateToDeviceView +import chat.simplex.common.views.migration.MigrationToState +import chat.simplex.common.views.newchat.darkStops +import chat.simplex.common.views.newchat.gradientPoints +import chat.simplex.common.views.newchat.lightStops import chat.simplex.common.views.onboarding.* +import chat.simplex.common.views.usersettings.DeleteImageButton +import chat.simplex.common.views.usersettings.EditImageButton import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch +import java.net.URI const val MAX_BIO_LENGTH_BYTES = 160 @@ -46,18 +63,63 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { val scrollState = rememberScrollState() val keyboardState by getKeyboardState() var savedKeyboardState by remember { mutableStateOf(keyboardState) } - Box( - modifier = Modifier - .fillMaxSize() - .padding(top = 20.dp) - ) { - val displayName = rememberSaveable { mutableStateOf("") } - val shortDescr = rememberSaveable { mutableStateOf("") } - val focusRequester = remember { FocusRequester() } + val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) + val displayName = rememberSaveable { mutableStateOf("") } + val shortDescr = rememberSaveable { mutableStateOf("") } + val chosenImage = rememberSaveable { mutableStateOf(null) } + val profileImage = rememberSaveable { mutableStateOf(null) } + val focusRequester = remember { FocusRequester() } + ModalBottomSheetLayout( + scrimColor = Color.Black.copy(alpha = 0.12F), + modifier = Modifier.imePadding(), + sheetContent = { + GetImageBottomSheet( + chosenImage, + onImageChange = { bitmap -> profileImage.value = resizeImageToStrSize(cropToSquare(bitmap), maxDataSize = 12500) }, + hideBottomSheet = { + scope.launch { bottomSheetModalState.hide() } + }) + }, + sheetState = bottomSheetModalState, + sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) + ) { + Box( + modifier = Modifier.fillMaxSize() + ) { ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING_HALF) + Row( + Modifier + .fillMaxWidth() + .padding(vertical = DEFAULT_PADDING_HALF), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = if (BuildConfigCommon.SIMPLEX_ASSETS) Modifier.padding(horizontal = 3.dp) else Modifier, + contentAlignment = Alignment.Center + ) { + Box(contentAlignment = Alignment.TopEnd) { + Box(contentAlignment = Alignment.Center) { + ProfileImage(128.dp, image = profileImage.value) + EditImageButton { scope.launch { bottomSheetModalState.show() } } + } + if (profileImage.value != null) { + DeleteImageButton { profileImage.value = null } + } + } + } + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isInDarkTheme()) MR.images.create_profile_light else MR.images.create_profile), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.height(140.dp) + ) + } + } Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.create_profile), withPadding = false, bottomPadding = DEFAULT_PADDING) Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( stringResource(MR.strings.display_name), @@ -100,9 +162,9 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { iconColor = MaterialTheme.colors.primary, click = { if (chatModel.localUserCreated.value == true) { - createProfileInProfiles(chatModel, displayName.value, shortDescr.value, close) + createProfileInProfiles(chatModel, displayName.value, shortDescr.value, profileImage.value, close) } else { - createProfileInNoProfileSetup(displayName.value, close) + createProfileInNoProfileSetup(displayName.value, profileImage.value, close) } }, ) @@ -123,49 +185,119 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { } } } + } } @Composable fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { - val scope = rememberCoroutineScope() - val scrollState = rememberScrollState() - val keyboardState by getKeyboardState() - var savedKeyboardState by remember { mutableStateOf(keyboardState) } - CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { - ModalView({ - if (chatModel.users.none { !it.user.hidden }) { - appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - } else { - close() + if (appPlatform.isDesktop) { + CreateFirstProfileDesktop(chatModel, close) + } else { + CreateFirstProfileMobile(chatModel, close) + } +} + +@Composable +private fun RowScope.MigrateButton(refocusTrigger: MutableState) { + val focusManager = LocalFocusManager.current + TextButton( + onClick = { + focusManager.clearFocus() + if (chatModel.migrationState.value == null) { + chatModel.migrationState.value = MigrationToState.PasteOrScanLink } - }) { - ColumnWithScrollBar { - val displayName = rememberSaveable { mutableStateOf("") } - val focusRequester = remember { FocusRequester() } - Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(start = DEFAULT_ONBOARDING_HORIZONTAL_PADDING * 2, end = DEFAULT_ONBOARDING_HORIZONTAL_PADDING * 2, bottom = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { - Box(Modifier.align(Alignment.CenterHorizontally)) { - AppBarTitle(stringResource(MR.strings.create_your_profile), bottomPadding = DEFAULT_PADDING, withPadding = false) - } - ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) - Spacer(Modifier.height(DEFAULT_PADDING)) - ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) - Spacer(Modifier.height(DEFAULT_PADDING)) - ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester) + ModalManager.fullscreen.showCustomModal(animated = false, forceAnimated = appPlatform.isDesktop) { close -> + MigrateToDeviceView { + close() + refocusTrigger.value++ } - Spacer(Modifier.fillMaxHeight().weight(1f)) - Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + } + }, + modifier = Modifier.padding(end = DEFAULT_PADDING_HALF) + ) { + Icon(painterResource(MR.images.ic_download), null, Modifier.size(22.dp), tint = MaterialTheme.colors.primary) + Spacer(Modifier.width(4.dp)) + Text( + stringResource(if (appPlatform.isDesktop) MR.strings.migrate_from_another_device else MR.strings.migrate), + color = MaterialTheme.colors.primary, fontWeight = FontWeight.Medium + ) + } +} + +private fun onboardingBackAction(chatModel: ChatModel, close: () -> Unit) { + if (chatModel.users.none { !it.user.hidden }) { + appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } else { + close() + } +} + +@Composable +private fun CreateFirstProfileMobile(chatModel: ChatModel, close: () -> Unit) { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + val focusRequester = remember { FocusRequester() } + val refocusTrigger = remember { mutableStateOf(0) } + ModalView( + close = { onboardingBackAction(chatModel, close) }, + endButtons = { MigrateButton(refocusTrigger) } + ) { + val displayName = rememberSaveable { mutableStateOf("") } + val keyboardState by getKeyboardState() + val imageHeightModifier = if (keyboardState == KeyboardState.Opened) { + Modifier.heightIn(max = 100.dp) + } else { + Modifier + } + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING), horizontalAlignment = Alignment.CenterHorizontally, maxIntrinsicSize = true) { + Spacer(Modifier.weight(1f)) + + OnboardingImage( + MR.images.your_profile, MR.images.your_profile_light, MR.images.ic_person, + modifier = Modifier + .then(if (keyboardState != KeyboardState.Opened) Modifier.fillMaxWidth() else Modifier) + .then(imageHeightModifier) + ) + + Text( + stringResource(MR.strings.onboarding_your_profile), + style = MaterialTheme.typography.h1, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF) + ) + Text( + stringResource(MR.strings.onboarding_on_your_phone), + style = MaterialTheme.typography.h3, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colors.secondary, + lineHeight = 25.sp, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 14.dp) + ) + Text( + stringResource(MR.strings.onboarding_no_account), + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.secondary, + textAlign = TextAlign.Center, + lineHeight = 20.sp, + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF) + ) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + ProfileNameField(displayName, stringResource(MR.strings.enter_profile_name), { it.trim() == mkValidName(it) }, focusRequester) + + Spacer(Modifier.weight(1f)) + + Column(Modifier.widthIn(max = 450.dp).padding(bottom = DEFAULT_PADDING * 2).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { OnboardingActionButton( - if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), - labelId = MR.strings.create_profile_button, + Modifier.fillMaxWidth(), + labelId = MR.strings.create_profile, onboarding = null, enabled = canCreateProfile(displayName.value), - onclick = { createProfileOnboarding(chat.simplex.common.platform.chatModel, displayName.value, close) } + onclick = { createProfileOnboarding(chatModel, displayName.value, close) } ) - // Reserve space - TextButtonBelowOnboardingButton("", null) } - LaunchedEffect(Unit) { + LaunchedEffect(refocusTrigger.value) { delay(300) focusRequester.requestFocus() } @@ -173,21 +305,57 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { LaunchedEffect(Unit) { setLastVersionDefault(chatModel) } - if (savedKeyboardState != keyboardState) { - LaunchedEffect(keyboardState) { - scope.launch { - savedKeyboardState = keyboardState - scrollState.animateScrollTo(scrollState.maxValue) - } - } - } } } } -fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) { +@Composable +private fun CreateFirstProfileDesktop(chatModel: ChatModel, close: () -> Unit) { + val focusRequester = remember { FocusRequester() } + val refocusTrigger = remember { mutableStateOf(0) } + val displayName = rememberSaveable { mutableStateOf("") } + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView( + close = { onboardingBackAction(chatModel, close) }, + endButtons = { MigrateButton(refocusTrigger) } + ) { + ColumnWithScrollBar(horizontalAlignment = Alignment.CenterHorizontally) { + Column(Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + Box(Modifier.align(Alignment.CenterHorizontally)) { + AppBarTitle(stringResource(MR.strings.onboarding_your_profile), bottomPadding = DEFAULT_PADDING, withPadding = false, overrideTitleColor = MaterialTheme.colors.onBackground, textAlign = TextAlign.Center, lineHeight = 42.sp) + } + Text(stringResource(MR.strings.onboarding_on_your_phone), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Medium, color = MaterialTheme.colors.secondary, lineHeight = 25.sp, textAlign = TextAlign.Center) + Spacer(Modifier.height(DEFAULT_PADDING)) + ReadableText(MR.strings.onboarding_no_account, TextAlign.Center, style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.secondary)) + Spacer(Modifier.height(DEFAULT_PADDING)) + ProfileNameField(displayName, stringResource(MR.strings.enter_profile_name), { it.trim() == mkValidName(it) }, focusRequester) + } + Spacer(Modifier.fillMaxHeight().weight(1f)) + Column(Modifier.widthIn(max = 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingActionButton( + Modifier.widthIn(min = 300.dp), + labelId = MR.strings.create_profile, + onboarding = null, + enabled = canCreateProfile(displayName.value), + onclick = { createProfileOnboarding(chatModel, displayName.value, close) } + ) + TextButtonBelowOnboardingButton("", null) + } + } + LaunchedEffect(Unit) { + setLastVersionDefault(chatModel) + } + } + } + LaunchedEffect(refocusTrigger.value) { + delay(300) + focusRequester.requestFocus() + } +} + +fun createProfileInNoProfileSetup(displayName: String, image: String? = null, close: () -> Unit) { withBGApi { - val user = controller.apiCreateActiveUser(null, Profile(displayName.trim(), "", null, null)) ?: return@withBGApi + val user = controller.apiCreateActiveUser(null, Profile(displayName.trim(), "", null, image)) ?: return@withBGApi if (!chatModel.connectedToRemote()) { chatModel.localUserCreated.value = true } @@ -198,16 +366,16 @@ fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) { } } -fun createProfileInProfiles(chatModel: ChatModel, displayName: String, shortDescr: String, close: () -> Unit) { +fun createProfileInProfiles(chatModel: ChatModel, displayName: String, shortDescr: String, image: String? = null, close: () -> Unit) { withBGApi { val rhId = chatModel.remoteHostId() val user = chatModel.controller.apiCreateActiveUser( - rhId, Profile(displayName.trim(), "", shortDescr.trim().ifEmpty { null }, null) + rhId, Profile(displayName.trim(), "", shortDescr.trim().ifEmpty { null }, image) ) ?: return@withBGApi chatModel.currentUser.value = user if (chatModel.users.isEmpty()) { chatModel.controller.startChat(user) - chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode) + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_NetworkCommitments) } else { val users = chatModel.controller.listUsers(rhId) chatModel.users.clear() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 117b8955a1..af58996393 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -211,7 +211,7 @@ fun ChatView( withContext(Dispatchers.Main) { ChannelRelaysModel.set(cInfo.groupInfo.groupId, relays) } - } else { + } else if (cInfo.groupInfo.membership.memberCurrent) { val gInfo = chatModel.controller.apiGetUpdatedGroupLinkData(chatRh, cInfo.groupInfo.groupId) if (gInfo != null) { withContext(Dispatchers.Main) { @@ -1953,7 +1953,7 @@ fun BoxScope.ChatItemsList( } false } - val swipeableModifier = if (appPlatform.isDesktop) Modifier else SwipeToDismissModifier( + val swipeableModifier = if (appPlatform.isDesktop || !chatInfo.sendMsgEnabled) Modifier else SwipeToDismissModifier( state = dismissState, directions = setOf(DismissDirection.EndToStart), swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, @@ -2339,7 +2339,7 @@ fun BoxScope.ChatItemsList( } val manager = LocalSelectionManager.current - val modifier = if (appPlatform.isDesktop && manager != null) SelectionHandler(manager, listState, mergedItems, linkMode) else Modifier + val modifier = if (appPlatform.isDesktop && manager != null) SelectionHandler(manager, listState, mergedItems, revealedItems, linkMode) else Modifier LazyColumnWithScrollBar( modifier.align(Alignment.BottomCenter), @@ -3204,7 +3204,7 @@ fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: ( val link = chatModel.controller.apiGetGroupLink(rhId, groupInfo.groupId) close?.invoke() ModalManager.end.showModalCloseable(true) { - GroupLinkView(chatModel, rhId, groupInfo, link, onGroupLinkUpdated = null, isChannel = groupInfo.useRelays) + GroupLinkView(chatModel, rhId, groupInfo, link, onGroupLinkUpdated = null, isChannel = groupInfo.useRelays, shareGroupInfo = groupInfo) } } } @@ -3597,7 +3597,6 @@ fun providerForGallery( override fun scrollToStart() { initialIndex = 0 - initialChatId = chatItems.firstOrNull { canShowMedia(it) }?.id ?: return } override fun onDismiss(index: Int) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeChatLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeChatLinkView.kt new file mode 100644 index 0000000000..14edea3ed6 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeChatLinkView.kt @@ -0,0 +1,53 @@ +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.MsgChatLink +import chat.simplex.common.ui.theme.appColors +import chat.simplex.common.views.helpers.ProfileImage +import dev.icerock.moko.resources.compose.painterResource +import chat.simplex.res.MR + +@Composable +fun ComposeChatLinkView( + chatLink: MsgChatLink, + cancelEnabled: Boolean, + cancelPreview: () -> Unit +) { + val sentColor = MaterialTheme.appColors.sentMessage + Row( + Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .background(sentColor) + .padding(start = 8.dp, top = 6.dp, bottom = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + ProfileImage(size = 54.dp, image = chatLink.image, icon = chatLink.iconRes) + Column( + Modifier.fillMaxWidth().weight(1f).padding(horizontal = 8.dp) + ) { + Text(chatLink.displayName, maxLines = 1, overflow = TextOverflow.Ellipsis) + chatLink.shortDescription?.let { descr -> + Text( + descr, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.secondary, + ) + } + } + if (cancelEnabled) { + IconButton(onClick = cancelPreview) { + Icon(painterResource(MR.images.ic_close), null, tint = MaterialTheme.colors.primary) + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 10f426b152..d0782f6bb4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -1,6 +1,7 @@ @file:UseSerializers(UriSerializer::class, ComposeMessageSerializer::class) package chat.simplex.common.views.chat +import SectionItemView import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -19,6 +20,8 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -57,6 +60,7 @@ const val MAX_NUMBER_OF_MENTIONS = 3 sealed class ComposePreview { @Serializable object NoPreview: ComposePreview() @Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview() + @Serializable class ChatLinkPreview(val chatLink: MsgChatLink, val ownerSig: LinkOwnerSig? = null): ComposePreview() @Serializable class MediaPreview(val images: List, val content: List): 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() @@ -112,7 +116,12 @@ data class ComposeState( val mentions: MentionedMembers = emptyMap() ) { constructor(editingItem: ChatItem, liveMessage: LiveMessage? = null, useLinkPreviews: Boolean): this( - ComposeMessage(editingItem.content.text), + ComposeMessage( + when (val mc = editingItem.content.msgContent) { + is MsgContent.MCChat -> stripTextLink(mc.text, mc.chatLink.connLinkStr) + else -> editingItem.content.text + } + ), editingItem.formattedText ?: FormattedText.plain(editingItem.content.text), liveMessage, chatItemPreview(editingItem), @@ -163,6 +172,7 @@ data class ComposeState( val hasContent = when (preview) { is ComposePreview.MediaPreview -> true is ComposePreview.VoicePreview -> true + is ComposePreview.ChatLinkPreview -> true is ComposePreview.FilePreview -> true else -> !whitespaceOnly || forwarding || liveMessage != null || submittingValidReport } @@ -174,6 +184,7 @@ data class ComposeState( val linkPreviewAllowed: Boolean get() = when (preview) { + is ComposePreview.ChatLinkPreview -> false is ComposePreview.MediaPreview -> false is ComposePreview.VoicePreview -> false is ComposePreview.FilePreview -> false @@ -200,6 +211,7 @@ data class ComposeState( get() = when (preview) { ComposePreview.NoPreview -> false is ComposePreview.CLinkPreview -> false + is ComposePreview.ChatLinkPreview -> false is ComposePreview.MediaPreview -> preview.content.isNotEmpty() is ComposePreview.VoicePreview -> false is ComposePreview.FilePreview -> true @@ -390,20 +402,46 @@ fun ComposeView( val recState: MutableState = remember { mutableStateOf(RecordingState.NotStarted) } AttachmentSelection(composeState, attachmentOption, composeState::processPickedFile) { uris, text -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(uris, text) } } + suspend fun fetchAndUpdateLinkPreview(url: String) { + composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null)) + val lp = getLinkPreview(url) + if (lp != null && pendingLinkUrl.value == url) { + composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(lp)) + pendingLinkUrl.value = null + } else if (pendingLinkUrl.value == url) { + composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) + pendingLinkUrl.value = null + } + } + fun loadLinkPreview(url: String, wait: Long? = null) { if (pendingLinkUrl.value == url) { - composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null)) withLongRunningApi(slow = 60_000) { if (wait != null) delay(wait) - val lp = getLinkPreview(url) - if (lp != null && pendingLinkUrl.value == url) { - chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.set(false) // to avoid showing alert to current users, show alert in v6.5 - composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(lp)) - pendingLinkUrl.value = null - } else if (pendingLinkUrl.value == url) { - composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) - pendingLinkUrl.value = null + if (pendingLinkUrl.value != url) return@withLongRunningApi + if (chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.get()) { + val socksEnabled = chatModel.controller.appPrefs.networkUseSocksProxy.get() + showLinkPreviewsConfirmAlert(socksEnabled) { enable -> + if (enable != null) { + chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.set(false) + chatModel.controller.appPrefs.privacyLinkPreviews.set(enable) + if (enable) { + withLongRunningApi(slow = 60_000) { fetchAndUpdateLinkPreview(url) } + } else if (pendingLinkUrl.value == url) { + composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) + pendingLinkUrl.value = null + } + } else { + cancelledLinks.add(url) + if (pendingLinkUrl.value == url) { + composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) + pendingLinkUrl.value = null + } + } + } + return@withLongRunningApi } + fetchAndUpdateLinkPreview(url) } } } @@ -468,6 +506,7 @@ fun ComposeView( is SharedContent.File -> listOf(shared.uri.toString()) is SharedContent.Text -> emptyList() is SharedContent.Forward -> emptyList() + is SharedContent.ChatLink -> emptyList() } // When sharing a file and pasting it in SimpleX itself, the file shouldn't be deleted before sending or before leaving the chat after sharing chatModel.filesToDelete.removeAll { file -> @@ -495,7 +534,7 @@ fun ComposeView( type = cInfo.chatType, id = cInfo.apiId, scope = cInfo.groupChatScope(), - sendAsGroup = (cInfo as? ChatInfo.Group)?.groupInfo?.let { it.useRelays && it.membership.memberRole >= GroupMemberRole.Owner } ?: false, + sendAsGroup = cInfo.sendAsGroup, live = live, ttl = ttl, composedMessages = listOf(ComposedMessage(file, quoted, mc, mentions)) @@ -626,7 +665,7 @@ fun ComposeView( toChatType = chat.chatInfo.chatType, toChatId = chat.chatInfo.apiId, toScope = chat.chatInfo.groupChatScope(), - sendAsGroup = (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.let { it.useRelays && it.membership.memberRole >= GroupMemberRole.Owner } ?: false, + sendAsGroup = chat.chatInfo.sendAsGroup, fromChatType = fromChatInfo.chatType, fromChatId = fromChatInfo.apiId, fromScope = fromChatInfo.groupChatScope(), @@ -672,8 +711,11 @@ fun ComposeView( is MsgContent.MCVoice -> MsgContent.MCVoice(msgText, duration = msgContent.duration) is MsgContent.MCFile -> MsgContent.MCFile(msgText) is MsgContent.MCReport -> MsgContent.MCReport(msgText, reason = msgContent.reason) - // TODO [short links] update chat link - is MsgContent.MCChat -> MsgContent.MCChat(msgText, chatLink = msgContent.chatLink) + is MsgContent.MCChat -> { + val linkStr = msgContent.chatLink.connLinkStr + val text = if (msgText.isEmpty()) linkStr else "$msgText\n$linkStr" + MsgContent.MCChat(text, chatLink = msgContent.chatLink, ownerSig = msgContent.ownerSig) + } is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = msgText, json = msgContent.json) } } @@ -760,6 +802,11 @@ fun ComposeView( when (val preview = cs.preview) { ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText)) is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview()) + is ComposePreview.ChatLinkPreview -> { + val linkStr = preview.chatLink.connLinkStr + val text = if (msgText.isEmpty()) linkStr else "$msgText\n$linkStr" + msgs.add(MsgContent.MCChat(text, preview.chatLink, preview.ownerSig)) + } is ComposePreview.MediaPreview -> { // TODO batch send: batch media previews preview.content.forEachIndexed { index, it -> @@ -1060,6 +1107,11 @@ fun ComposeView( ::cancelLinkPreview, cancelEnabled = !composeState.value.inProgress ) + is ComposePreview.ChatLinkPreview -> ComposeChatLinkView( + chatLink = preview.chatLink, + cancelEnabled = !composeState.value.inProgress, + cancelPreview = { composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) } + ) is ComposePreview.MediaPreview -> ComposeImageView( preview, ::cancelImages, @@ -1128,8 +1180,10 @@ fun ComposeView( } } - val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled) - val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason) + val ownerRelayState = ownerRelayState(chat, chatModel) + + val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason(ownerRelayState?.noActiveRelays == true)) + val sendMsgEnabled = rememberUpdatedState(userCantSendReason.value == null) val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) @Composable @@ -1250,6 +1304,8 @@ fun ComposeView( composeState.value = cs.copy(inProgress = false, progressByTimeout = false) } else if (!cs.empty) { if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) { + recState.value = RecordingState.NotStarted + RecorderInterface.stopRecording?.invoke() composeState.value = cs.copy(preview = cs.preview.copy(finished = true)) } if (saveLastDraft) { @@ -1307,7 +1363,7 @@ fun ComposeView( sendButtonColor = sendButtonColor, timedMessageAllowed = timedMessageAllowed, customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime, - placeholder = placeholder ?: composeState.value.placeholder, + placeholder = if (userCantSendReason.value != null) "" else placeholder ?: composeState.value.placeholder, sendMessage = { ttl -> sendMessage(ttl) resetLinkPreview() @@ -1438,6 +1494,22 @@ fun ComposeView( contextItem = ComposeContextItem.ForwardingItems(shared.chatItems, shared.fromChatInfo), preview = if (composeState.value.preview is ComposePreview.CLinkPreview) composeState.value.preview else ComposePreview.NoPreview ) + is SharedContent.ChatLink -> { + val cInfo = chat.chatInfo + val sendAsGroup = cInfo.sendAsGroup + withBGApi { + val mc = chatModel.controller.apiShareChatMsgContent( + chat.remoteHostId, ChatType.Group, shared.groupInfo.groupId, + cInfo.chatType, cInfo.apiId, + cInfo.groupChatScope(), sendAsGroup + ) + if (mc is MsgContent.MCChat) { + composeState.value = composeState.value.copy( + preview = ComposePreview.ChatLinkPreview(mc.chatLink, mc.ownerSig) + ) + } + } + } null -> {} } chatModel.sharedContent.value = null @@ -1470,26 +1542,24 @@ fun ComposeView( && gInfo.membership.memberStatus !in listOf(GroupMemberStatus.MemRejected, GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) ) { if (gInfo.membership.memberRole == GroupMemberRole.Owner) { - val relays = if (ChannelRelaysModel.groupId.value == gInfo.groupId) ChannelRelaysModel.groupRelays.toList() else emptyList() - val failedCount = relays.count { relayMemberConnFailed(chatModel, it) != null } - val activeCount = relays.count { it.relayStatus == RelayStatus.RsActive && relayMemberConnFailed(chatModel, it) == null } - if (relays.isNotEmpty() && activeCount < relays.size) { - OwnerChannelRelayBar(chatModel, relays, activeCount, failedCount, relayListExpanded) + ownerRelayState?.let { s -> + if (s.relays.isEmpty() || s.activeCount < s.relays.size) { + OwnerChannelRelayBar(chatModel, s.relays, s.activeCount, s.failedCount, s.removedCount, relayListExpanded) + } } } else { val hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?: emptyList()).sorted() val relayMembers = chatModel.groupMembers.value - .filter { it.memberRole == GroupMemberRole.Relay } + .filter { it.memberRole == GroupMemberRole.Relay && it.memberStatus !in listOf(GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) } .sortedBy { hostFromRelayLink(it.relayLink ?: "") } val showProgress = !gInfo.nextConnectPrepared || composeState.value.inProgress - val connectedCount = relayMembers.count { it.activeConn?.connStatus == ConnStatus.Ready } - val deletedCount = relayMembers.count { it.activeConn?.connStatus == ConnStatus.Deleted } - val failedCount = relayMembers.count { it.activeConn?.connFailedErr != null } - val errorCount = deletedCount + failedCount - val resolvedCount = connectedCount + deletedCount + val removedCount = relayMembers.count { relayMemberRemoved(it.memberStatus) } + val connectedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connStatus == ConnStatus.Ready && it.activeConn?.connFailedErr == null } + val failedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connFailedErr != null } + val resolvedCount = connectedCount + removedCount + failedCount val total = if (relayMembers.isNotEmpty()) relayMembers.size else hostnames.size - if (total > 0 && (!showProgress || resolvedCount < total)) { - SubscriberChannelRelayBar(hostnames, relayMembers, connectedCount, errorCount, total, showProgress, relayListExpanded) + if (total == 0 || removedCount + failedCount > 0 || resolvedCount < total) { + SubscriberChannelRelayBar(hostnames, relayMembers, connectedCount, removedCount, failedCount, total, showProgress, relayListExpanded) } } } @@ -1623,7 +1693,7 @@ fun ComposeView( Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { AttachmentAndCommandsButtons() val broadcastPlaceholder = (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.let { gi -> - if (gi.useRelays && gi.membership.memberRole >= GroupMemberRole.Owner) generalGetString(MR.strings.compose_view_broadcast) + if (gi.useRelays && gi.membership.memberRole >= GroupMemberRole.Owner && chat.chatInfo.groupChatScope() == null) generalGetString(MR.strings.compose_view_broadcast) else null } SendMsgView_(disableSendButton = disableSendButton, placeholder = broadcastPlaceholder) @@ -1633,31 +1703,115 @@ fun ComposeView( } } +private fun showLinkPreviewsConfirmAlert(socksEnabled: Boolean, onChoice: (Boolean?) -> Unit) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.link_previews_alert_title), + text = AnnotatedString( + if (socksEnabled) + generalGetString(MR.strings.link_previews_alert_desc) + "\n\n" + generalGetString(MR.strings.link_previews_alert_desc_socks) + else + generalGetString(MR.strings.link_previews_alert_desc) + ), + onDismissRequest = { onChoice(null) }, + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + onChoice(false) + }) { + Text(stringResource(MR.strings.link_previews_alert_disable), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + onChoice(true) + }) { + Text(stringResource(MR.strings.link_previews_alert_enable), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = if (socksEnabled) MaterialTheme.colors.primary else Color.Red) + } +// SectionItemView({ +// AlertManager.shared.hideAlert() +// onChoice(null) +// }) { +// Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.onBackground) +// } + } + } + ) +} + @Composable private fun OwnerChannelRelayBar( chatModel: ChatModel, relays: List, activeCount: Int, failedCount: Int, + removedCount: Int, relayListExpanded: MutableState ) { val total = relays.size - val sorted = relays.sortedBy { relayDisplayName(it) } + val allBroken = activeCount == 0 && (failedCount + removedCount) == total + val members = chatModel.groupMembers.value.associateBy { it.groupMemberId } + val sorted = relays.map { relay -> relay to members[relay.groupMemberId] }.sortedBy { relayDisplayName(it.first) } Column(Modifier.background(MaterialTheme.colors.surface)) { RelayBarHeader(relayListExpanded) { - if (activeCount + failedCount < total) { + if (!allBroken && activeCount + failedCount + removedCount < total) { RelayProgressIndicator(active = activeCount, total = total) } - val statusText = if (failedCount > 0) { - String.format(generalGetString(MR.strings.relay_bar_active_with_failures), activeCount, total, failedCount) + if (total == 0) { + Text(generalGetString(MR.strings.relay_bar_no_relays), color = MaterialTheme.colors.secondary) + Icon( + painterResource(MR.images.ic_warning), + contentDescription = null, + tint = WarningOrange, + modifier = Modifier.size(18.dp) + ) + } else if (allBroken) { + val statusText = if (removedCount == total) { + generalGetString(MR.strings.relay_bar_all_relays_removed) + } else if (failedCount == total) { + generalGetString(MR.strings.relay_bar_all_relays_failed) + } else { + generalGetString(MR.strings.relay_bar_no_active_relays) + } + Text(statusText, color = MaterialTheme.colors.secondary) + Icon( + painterResource(MR.images.ic_warning), + contentDescription = null, + tint = WarningOrange, + modifier = Modifier.size(18.dp) + ) + } else if (activeCount + failedCount + removedCount >= total) { + val statusText = if (failedCount > 0 && removedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_relays_not_active), failedCount + removedCount) + } else if (failedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_relays_failed), failedCount) + } else { + String.format(generalGetString(MR.strings.relay_bar_relays_removed), removedCount) + } + Text(statusText, color = MaterialTheme.colors.secondary) } else { - String.format(generalGetString(MR.strings.relay_bar_active), activeCount, total) + val statusText = if (failedCount > 0 && removedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_active_with_errors), activeCount, total, failedCount + removedCount) + } else if (failedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_active_with_failures), activeCount, total, failedCount) + } else if (removedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_active_with_removed), activeCount, total, removedCount) + } else { + String.format(generalGetString(MR.strings.relay_bar_active), activeCount, total) + } + Text(statusText, color = MaterialTheme.colors.secondary) } - Text(statusText, modifier = Modifier.weight(1f), color = MaterialTheme.colors.secondary) } if (relayListExpanded.value) { - sorted.forEach { relay -> - val failedErr = relayMemberConnFailed(chatModel, relay) + if (allBroken) { + Text( + generalGetString(MR.strings.relay_bar_owner_no_delivery), + modifier = Modifier.fillMaxWidth().padding(start = 12.dp, end = DEFAULT_PADDING, bottom = 4.dp), + color = MaterialTheme.colors.secondary, + fontSize = 12.sp + ) + } + sorted.forEach { (relay, m) -> + val failedErr = m?.activeConn?.connFailedErr RelayBarDetailRow( onClick = if (failedErr != null) { { @@ -1674,7 +1828,7 @@ private fun OwnerChannelRelayBar( fontSize = 12.sp ) Spacer(Modifier.weight(1f)) - RelayStatusIndicator(relay.relayStatus, connFailed = failedErr != null) + RelayStatusIndicator(relay.relayStatus, connFailed = failedErr != null, memberStatus = m?.memberStatus) } } } @@ -1686,28 +1840,73 @@ private fun SubscriberChannelRelayBar( hostnames: List, relayMembers: List, connectedCount: Int, - errorCount: Int, + removedCount: Int, + failedCount: Int, total: Int, showProgress: Boolean, relayListExpanded: MutableState ) { + val errorCount = removedCount + failedCount + val allBroken = connectedCount == 0 && errorCount == total Column(Modifier.background(MaterialTheme.colors.surface)) { RelayBarHeader(relayListExpanded) { - if (showProgress && connectedCount + errorCount < total) { - RelayProgressIndicator(active = connectedCount, total = total) - } - val statusText = if (showProgress) { - if (errorCount > 0) { + if (total == 0) { + Text(generalGetString(MR.strings.relay_bar_no_relays), color = MaterialTheme.colors.secondary) + Icon( + painterResource(MR.images.ic_warning), + contentDescription = null, + tint = WarningOrange, + modifier = Modifier.size(18.dp) + ) + } else if (allBroken) { + val statusText = if (removedCount == total) { + generalGetString(MR.strings.relay_bar_all_relays_removed) + } else if (failedCount == total) { + generalGetString(MR.strings.relay_bar_all_relays_failed) + } else { + generalGetString(MR.strings.relay_bar_no_active_relays) + } + Text(statusText, color = MaterialTheme.colors.secondary) + Icon( + painterResource(MR.images.ic_warning), + contentDescription = null, + tint = WarningOrange, + modifier = Modifier.size(18.dp) + ) + } else if (connectedCount + removedCount + failedCount >= total && errorCount > 0) { + val statusText = if (failedCount > 0 && removedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_relays_not_active), failedCount + removedCount) + } else if (failedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_relays_failed), failedCount) + } else { + String.format(generalGetString(MR.strings.relay_bar_relays_removed), removedCount) + } + Text(statusText, color = MaterialTheme.colors.secondary) + } else { + if (showProgress && connectedCount + errorCount < total) { + RelayProgressIndicator(active = connectedCount, total = total) + } + val statusText = if (failedCount > 0 && removedCount > 0) { String.format(generalGetString(MR.strings.relay_bar_connected_with_errors), connectedCount, total, errorCount) + } else if (failedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_connected_with_failures), connectedCount, total, failedCount) + } else if (removedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_connected_with_removed), connectedCount, total, removedCount) } else { String.format(generalGetString(MR.strings.relay_bar_connected), connectedCount, total) } - } else { - String.format(generalGetString(MR.strings.relay_bar_count), total) + Text(statusText, color = MaterialTheme.colors.secondary) } - Text(statusText, modifier = Modifier.weight(1f), color = MaterialTheme.colors.secondary) } if (relayListExpanded.value) { + if (allBroken) { + Text( + generalGetString(MR.strings.relay_bar_subscriber_waiting), + modifier = Modifier.fillMaxWidth().padding(start = 12.dp, end = DEFAULT_PADDING, bottom = 4.dp), + color = MaterialTheme.colors.secondary, + fontSize = 12.sp + ) + } if (relayMembers.isEmpty()) { hostnames.forEach { relay -> RelayBarDetailRow { @@ -1775,6 +1974,7 @@ private fun RelayBarHeader( verticalAlignment = Alignment.CenterVertically ) { content() + Spacer(Modifier.weight(1f)) Icon( painterResource(if (expanded.value) MR.images.ic_chevron_down else MR.images.ic_chevron_up), contentDescription = null, @@ -1800,9 +2000,31 @@ private fun RelayBarDetailRow( } } -private fun relayMemberConnFailed(chatModel: ChatModel, relay: GroupRelay): String? { - return chatModel.groupMembers.value - .firstOrNull { it.groupMemberId == relay.groupMemberId } - ?.activeConn?.connFailedErr +private fun ownerRelayState(chat: Chat, chatModel: ChatModel): OwnerRelayState? { + val gInfo = (chat.chatInfo as? ChatInfo.Group)?.groupInfo ?: return null + if (!gInfo.useRelays || gInfo.membership.memberRole != GroupMemberRole.Owner || + gInfo.membership.memberStatus in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) + ) return null + val relays = if (ChannelRelaysModel.groupId.value == gInfo.groupId) ChannelRelaysModel.groupRelays.toList() else emptyList() + if (relays.isEmpty()) return OwnerRelayState(emptyList(), 0, 0, 0, true) + val relayMembers = relays.map { relay -> + relay to chatModel.groupMembers.value.firstOrNull { it.groupMemberId == relay.groupMemberId } + } + val removedCount = relayMembers.count { (_, m) -> relayMemberRemoved(m?.memberStatus) } + val activeCount = relayMembers.count { (relay, m) -> !relayMemberRemoved(m?.memberStatus) && relay.relayStatus == RelayStatus.RsActive && m?.activeConn?.connFailedErr == null } + val failedCount = relayMembers.count { (_, m) -> !relayMemberRemoved(m?.memberStatus) && m?.activeConn?.connFailedErr != null } + val noActiveRelays = activeCount == 0 && (failedCount + removedCount) == relays.size + return OwnerRelayState(relays, activeCount, failedCount, removedCount, noActiveRelays) } +private data class OwnerRelayState( + val relays: List, + val activeCount: Int, + val failedCount: Int, + val removedCount: Int, + val noActiveRelays: Boolean +) + +private fun relayMemberRemoved(status: GroupMemberStatus?): Boolean = + status in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) + diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt index 1501fb7938..e681c4fed7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt @@ -39,7 +39,7 @@ fun ContextItemView( val receivedColor = MaterialTheme.appColors.receivedMessage @Composable - fun MessageText(contextItem: ChatItem, attachment: ImageResource?, lines: Int) { + fun MessageText(contextItem: ChatItem, attachment: ImageResource?, lines: Int, prefix: AnnotatedString? = null, stripLink: String? = null) { val inlineContent: Pair Unit, Map>? = if (attachment != null) { remember(contextItem.id) { val inlineContentBuilder: AnnotatedString.Builder.() -> Unit = { @@ -68,24 +68,35 @@ fun ContextItemView( userMemberId = when { chatInfo is ChatInfo.Group -> chatInfo.groupInfo.membership.memberId else -> null - } + }, + prefix = prefix, + stripLink = stripLink, ) } fun attachment(contextItem: ChatItem): ImageResource? { val fileIsLoaded = getLoadedFilePath(contextItem.file) != null - return when (contextItem.content.msgContent) { + val mc = contextItem.content.msgContent + return when (mc) { is MsgContent.MCFile -> if (fileIsLoaded) MR.images.ic_draft_filled else null is MsgContent.MCImage -> MR.images.ic_image is MsgContent.MCVoice -> if (fileIsLoaded) MR.images.ic_play_arrow_filled else null + is MsgContent.MCChat -> mc.chatLink.smallIconRes else -> null } } @Composable fun ContextMsgPreview(contextItem: ChatItem, lines: Int) { - MessageText(contextItem, remember(contextItem.id) { attachment(contextItem) }, lines) + val mc = contextItem.content.msgContent + if (mc is MsgContent.MCChat) { + val hasText = contextItem.text != mc.chatLink.connLinkStr + val prefix = buildAnnotatedString { append(mc.chatLink.displayName + if (hasText) " - " else "") } + MessageText(contextItem, remember(contextItem.id) { mc.chatLink.smallIconRes }, lines, prefix = prefix, stripLink = mc.chatLink.connLinkStr) + } else { + MessageText(contextItem, remember(contextItem.id) { attachment(contextItem) }, lines) + } } val sent = contextItems[0].chatDir.sent diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 9184071c07..0948551c7e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -195,7 +195,7 @@ fun SendMsgView( ) } } - if (timedMessageAllowed) { + if (timedMessageAllowed && !cs.editing) { menuItems.add { ItemAction( generalGetString(MR.strings.disappearing_message), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/TextSelection.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/TextSelection.kt index 4447bf9da2..d85488cefc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/TextSelection.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/TextSelection.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.item.itemPrefixText import chat.simplex.common.views.chat.item.itemSegmentDisplayText import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @@ -52,8 +53,10 @@ val LocalItemContext = compositionLocalOf { ItemContext() } data class SelectionRange( val startIndex: Int, + val startItemId: Long, val startOffset: Int, val endIndex: Int, + val endItemId: Long, val endOffset: Int ) @@ -79,11 +82,13 @@ class SelectionManager { var viewportPosition by mutableStateOf(Offset.Zero) var focusCharRect by mutableStateOf(Rect.Zero) // X: absolute window, Y: relative to item var listState: State? = null + var mergedItemsState: State? = null var onCopySelection: (() -> Unit)? = null private var autoScrollJob: Job? = null fun startSelection(startIndex: Int, anchorY: Float, anchorX: Float) { - range = SelectionRange(startIndex, -1, startIndex, -1) + val id = mergedItemsState?.value?.items?.getOrNull(startIndex)?.newest()?.item?.id ?: return + range = SelectionRange(startIndex, id, -1, startIndex, id, -1) selectionState = SelectionState.Selecting anchorWindowY = anchorY anchorWindowX = anchorX @@ -96,7 +101,8 @@ class SelectionManager { fun updateFocusIndex(index: Int) { val r = range ?: return - range = r.copy(endIndex = index) + val id = mergedItemsState?.value?.items?.getOrNull(index)?.newest()?.item?.id ?: return + range = r.copy(endIndex = index, endItemId = id) } fun updateFocusOffset(offset: Int, charRect: Rect = Rect.Zero) { @@ -175,6 +181,15 @@ class SelectionManager { updateFocusIndex(idx) } + fun resyncIndices() { + val r = range ?: return + val items = mergedItemsState?.value?.items ?: return + val newStartIndex = items.indexOfFirst { it.newest().item.id == r.startItemId } + val newEndIndex = items.indexOfFirst { it.newest().item.id == r.endItemId } + if (newStartIndex < 0 || newEndIndex < 0) clearSelection() + else range = r.copy(startIndex = newStartIndex, endIndex = newEndIndex) + } + fun updateAutoScroll(draggingDown: Boolean, pointerY: Float, scope: CoroutineScope) { val edgeDistance = if (draggingDown) viewportBottom - pointerY else pointerY - viewportTop if (edgeDistance !in 0f..AUTO_SCROLL_ZONE_PX) { @@ -196,12 +211,13 @@ class SelectionManager { } } - fun getSelectedCopiedText(items: List, linkMode: SimplexLinkMode): String { + fun getSelectedCopiedText(items: List, revealedItems: Set, linkMode: SimplexLinkMode): String { val r = range ?: return "" val lo = minOf(r.startIndex, r.endIndex) val hi = maxOf(r.startIndex, r.endIndex) return (lo..hi).mapNotNull { idx -> val ci = items.getOrNull(idx)?.newest()?.item ?: return@mapNotNull null + if (ci.meta.itemDeleted != null && (!revealedItems.contains(ci.id) || ci.isDeletedContent)) return@mapNotNull null val sel = selectedRange(range, idx) ?: return@mapNotNull null selectedItemCopiedText(ci, sel, linkMode) }.reversed().joinToString("\n") @@ -239,15 +255,22 @@ fun selectedRange(range: SelectionRange?, index: Int): IntRange? { } // Extracts source text for the selected range within one item. -// Selection offsets are in display-text space. For transformed segments (mentions, links with showText), -// the full source is emitted if any part is selected. For untransformed segments, partial substring works. +// Selection offsets are in display-text space (which includes any leading itemPrefixText). +// For transformed segments (mentions, links with showText), the full source is emitted if any part +// is selected. For untransformed segments, partial substring works. private fun selectedItemCopiedText(ci: ChatItem, sel: IntRange, linkMode: SimplexLinkMode): String { - val formattedText = ci.formattedText ?: return ci.text.substring( - sel.first.coerceAtMost(ci.text.length), - (sel.last + 1).coerceAtMost(ci.text.length) - ) + val prefix = itemPrefixText(ci) val sb = StringBuilder() - var displayOffset = 0 + if (sel.first < prefix.length) { + sb.append(prefix, sel.first, minOf(prefix.length, sel.last + 1)) + } + val formattedText = ci.formattedText ?: run { + val start = (sel.first - prefix.length).coerceAtLeast(0).coerceAtMost(ci.text.length) + val end = (sel.last + 1 - prefix.length).coerceAtMost(ci.text.length) + if (start < end) sb.append(ci.text, start, end) + return sb.toString() + } + var displayOffset = prefix.length for (ft in formattedText) { val segDisplay = itemSegmentDisplayText(ft, ci, linkMode) val displayEnd = displayOffset + segDisplay.length @@ -268,7 +291,7 @@ private fun selectedItemCopiedText(ci: ChatItem, sel: IntRange, linkMode: Simple // Snaps a boundary offset to include full transformed segments. private fun snapOffset(ci: ChatItem, offset: Int, linkMode: SimplexLinkMode, expandRight: Boolean): Int { val formattedText = ci.formattedText ?: return offset - var displayOffset = 0 + var displayOffset = itemPrefixText(ci).length for (ft in formattedText) { val segDisplay = itemSegmentDisplayText(ft, ci, linkMode) val displayEnd = displayOffset + segDisplay.length @@ -291,6 +314,7 @@ fun BoxScope.SelectionHandler( manager: SelectionManager, listState: State, mergedItems: State, + revealedItems: State>, linkMode: SimplexLinkMode ): Modifier { val touchSlop = LocalViewConfiguration.current.touchSlop @@ -310,12 +334,15 @@ fun BoxScope.SelectionHandler( } manager.listState = listState + manager.mergedItemsState = mergedItems manager.onCopySelection = { - clipboard.setText(AnnotatedString(manager.getSelectedCopiedText(mergedItems.value.items, linkMode))) - manager.clearSelection() + clipboard.setText(AnnotatedString(manager.getSelectedCopiedText(mergedItems.value.items, revealedItems.value, linkMode))) showToast(generalGetString(MR.strings.copied)) } + // Resync after the items list mutates (new message arrives, item deleted). + SideEffect { manager.resyncIndices() } + return Modifier .focusRequester(focusRequester) .focusable() @@ -509,7 +536,10 @@ fun SelectionCopyButton() { .background(MaterialTheme.colors.surface, RoundedCornerShape(20.dp)) .border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(20.dp)) .clip(RoundedCornerShape(20.dp)) - .clickable { manager.onCopySelection?.invoke() } + .clickable { + manager.onCopySelection?.invoke() + manager.clearSelection() + } .padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt new file mode 100644 index 0000000000..d0c2486069 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt @@ -0,0 +1,237 @@ +package chat.simplex.common.views.chat.group + +import SectionBottomSpacer +import SectionCustomFooter +import SectionDividerSpaced +import SectionItemView +import SectionView +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.unit.dp +import androidx.compose.ui.unit.sp +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.chatRelayDisplayName +import chat.simplex.common.views.usersettings.SettingsActionItem +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import kotlinx.coroutines.launch + +data class AvailableRelay( + val relayId: Long, + val relay: UserChatRelay, + val operatorName: String? +) + +@Composable +fun AddGroupRelayView( + groupInfo: GroupInfo, + existingRelayIds: Set, + onRelayAdded: () -> Unit, + close: () -> Unit +) { + var availableRelays by remember { mutableStateOf>(emptyList()) } + var selectedRelayIds by remember { mutableStateOf>(emptySet()) } + var isLoading by remember { mutableStateOf(true) } + var isAdding by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + BackHandler(onBack = close) + + LaunchedEffect(Unit) { + try { + val servers = ChatController.getUserServers(null) + if (servers != null) { + val relays = mutableListOf() + for (op in servers) { + if (op.operator != null && op.operator.enabled != true) continue + val opName: String? = if (op.operator?.operatorTag != null) op.operator.tradeName else null + for (relay in op.chatRelays) { + val relayId = relay.chatRelayId + if (relay.enabled && !relay.deleted && relayId != null && relayId !in existingRelayIds) { + relays.add(AvailableRelay(relayId, relay, opName)) + } + } + } + availableRelays = relays + } + } catch (e: Exception) { + Log.e(TAG, "loadAvailableRelays error: ${e.message}") + } + isLoading = false + } + + AddGroupRelayLayout( + availableRelays = availableRelays, + selectedRelayIds = selectedRelayIds, + isLoading = isLoading, + isAdding = isAdding, + onToggleRelay = { relayId -> + selectedRelayIds = if (relayId in selectedRelayIds) selectedRelayIds - relayId else selectedRelayIds + relayId + }, + onAddRelays = { + val relayIds = selectedRelayIds.toList() + if (relayIds.isEmpty()) return@AddGroupRelayLayout + isAdding = true + scope.launch { + addSelectedRelays(groupInfo, relayIds, selectedRelayIds, availableRelays, onRelayAdded, close) { newSelectedIds, newAvailableRelays -> + selectedRelayIds = newSelectedIds + availableRelays = newAvailableRelays + isAdding = false + } + } + } + ) +} + +@Composable +private fun AddGroupRelayLayout( + availableRelays: List, + selectedRelayIds: Set, + isLoading: Boolean, + isAdding: Boolean, + onToggleRelay: (Long) -> Unit, + onAddRelays: () -> Unit +) { + ColumnWithScrollBar { + AppBarTitle(generalGetString(MR.strings.add_relays_title)) + + if (isLoading) { + Box(Modifier.fillMaxWidth().padding(vertical = DEFAULT_PADDING), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } else if (availableRelays.isEmpty()) { + SectionView { + SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text( + generalGetString(MR.strings.no_available_relays), + color = MaterialTheme.colors.secondary + ) + } + } + } else { + SectionView { + AddRelaysButton( + onClick = onAddRelays, + disabled = selectedRelayIds.isEmpty() || isAdding + ) + } + SectionCustomFooter { + val count = selectedRelayIds.size + Text( + if (count == 0) generalGetString(MR.strings.no_relays_selected) + else String.format(generalGetString(MR.strings.num_relays_selected), count), + color = MaterialTheme.colors.secondary, + lineHeight = 18.sp, + fontSize = 14.sp + ) + } + SectionDividerSpaced(maxTopPadding = true) + SectionView(generalGetString(MR.strings.select_relays).uppercase()) { + availableRelays.forEach { item -> + val selected = item.relayId in selectedRelayIds + SectionItemView( + click = { onToggleRelay(item.relayId) }, + padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = 4.dp) + ) { + Column(Modifier.weight(1f)) { + Text( + chatRelayDisplayName(item.relay), + maxLines = 1, + color = MaterialTheme.colors.onBackground + ) + if (item.operatorName != null) { + Text( + item.operatorName, + fontSize = 12.sp, + maxLines = 1, + color = MaterialTheme.colors.secondary + ) + } + } + Spacer(Modifier.width(8.dp)) + Icon( + painterResource(if (selected) MR.images.ic_check_circle_filled else MR.images.ic_circle), + contentDescription = null, + tint = if (selected) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + modifier = Modifier.size(24.dp) + ) + } + } + } + } + SectionBottomSpacer() + } +} + +@Composable +private fun AddRelaysButton(onClick: () -> Unit, disabled: Boolean) { + SettingsActionItem( + painterResource(MR.images.ic_check), + generalGetString(MR.strings.add_relays_title), + click = onClick, + textColor = MaterialTheme.colors.primary, + iconColor = MaterialTheme.colors.primary, + disabled = disabled, + ) +} + +private suspend fun addSelectedRelays( + groupInfo: GroupInfo, + relayIds: List, + selectedRelayIds: Set, + availableRelays: List, + onRelayAdded: () -> Unit, + close: () -> Unit, + updateState: (Set, List) -> Unit +) { + try { + val result = ChatController.apiAddGroupRelays(groupInfo.groupId, relayIds) + if (result == null) { + updateState(selectedRelayIds, availableRelays) + return + } + when (result) { + is ChatController.AddGroupRelaysResult.Added -> { + ChannelRelaysModel.set(groupId = result.groupInfo.groupId, groupRelays = result.groupRelays) + onRelayAdded() + close() + } + is ChatController.AddGroupRelaysResult.AddFailed -> { + val results = result.addRelayResults + val successIds = results.filter { it.relayError == null }.mapNotNull { it.relay.chatRelayId }.toSet() + var newSelectedIds = selectedRelayIds + var newAvailableRelays = availableRelays + if (successIds.isNotEmpty()) { + newSelectedIds = selectedRelayIds - successIds + newAvailableRelays = availableRelays.filter { it.relayId !in successIds } + onRelayAdded() + } + val errorLines = results.filter { it.relayError != null } + .map { "${chatRelayDisplayName(it.relay)}: ${it.relayError?.let { e -> ChatController.connErrorText(e) } ?: ""}" } + val successNames = results.filter { it.relayError == null } + .map { chatRelayDisplayName(it.relay) } + var msg = errorLines.joinToString("\n") + if (successNames.isNotEmpty()) { + msg += "\n" + String.format(generalGetString(MR.strings.relays_added_format), successNames.joinToString(", ")) + } + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.error_adding_relays), + text = msg + ) + updateState(newSelectedIds, newAvailableRelays) + } + } + } catch (e: Exception) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.error_adding_relays), + text = e.message ?: "" + ) + updateState(selectedRelayIds, availableRelays) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt index e8f2a36fff..891753aed8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt @@ -2,6 +2,7 @@ package chat.simplex.common.views.chat.group import SectionBottomSpacer import SectionItemView +import SectionItemViewLongClickable import SectionTextFooter import SectionView import androidx.compose.foundation.layout.* @@ -16,9 +17,11 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.chatlist.setGroupMembers import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource @Composable fun ChannelRelaysView( @@ -29,16 +32,18 @@ fun ChannelRelaysView( showMemberInfo: (GroupMember, GroupRelay?) -> Unit ) { BackHandler(onBack = close) - var groupRelays by remember { mutableStateOf>(emptyList()) } + val groupRelays = ChannelRelaysModel.groupRelays LaunchedEffect(Unit) { setGroupMembers(rhId, groupInfo, chatModel) if (groupInfo.isOwner) { - groupRelays = chatModel.controller.apiGetGroupRelays(groupInfo.groupId) + val relays = chatModel.controller.apiGetGroupRelays(groupInfo.groupId) + ChannelRelaysModel.set(groupId = groupInfo.groupId, groupRelays = relays) } } ChannelRelaysLayout( + rhId = rhId, groupInfo = groupInfo, chatModel = chatModel, groupRelays = groupRelays, @@ -48,13 +53,14 @@ fun ChannelRelaysView( @Composable private fun ChannelRelaysLayout( + rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, groupRelays: List, showMemberInfo: (GroupMember, GroupRelay?) -> Unit ) { val relayMembers = remember { chatModel.groupMembers }.value - .filter { it.memberRole == GroupMemberRole.Relay } + .filter { it.memberRole == GroupMemberRole.Relay && it.memberStatus != GroupMemberStatus.MemRemoved && it.memberStatus != GroupMemberStatus.MemGroupDeleted } ColumnWithScrollBar { AppBarTitle(generalGetString(MR.strings.channel_relays_title)) @@ -74,11 +80,24 @@ private fun ChannelRelaysLayout( if (index > 0) { Divider() } - SectionItemView( + val showMenu = remember { mutableStateOf(false) } + SectionItemViewLongClickable( click = { showMemberInfo(member, groupRelays.firstOrNull { it.groupMemberId == member.groupMemberId }) }, + longClick = { showMenu.value = true }, minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING) ) { + // TODO [relays] re-enable when relay management ships + /* + if (groupInfo.isOwner && member.canBeRemoved(groupInfo)) { + DefaultDropdownMenu(showMenu) { + ItemAction(generalGetString(MR.strings.button_remove_relay), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = { + removeMemberAlert(rhId, groupInfo, member) + showMenu.value = false + }) + } + } + */ val statusText = if (groupInfo.isOwner) { ownerRelayStatusText(member, groupRelays) } else { @@ -90,6 +109,35 @@ private fun ChannelRelaysLayout( } SectionTextFooter(generalGetString(MR.strings.chat_relays_forward_messages)) } + // TODO [relays] re-enable when relay management ships + /* + if (groupInfo.isOwner) { + SectionView { + SectionItemView(click = { + val existingRelayIds = groupRelays.filter { it.relayStatus != RelayStatus.RsInactive }.mapNotNull { it.userChatRelay.chatRelayId }.toSet() + ModalManager.end.showModalCloseable(true) { close -> + AddGroupRelayView( + groupInfo = groupInfo, + existingRelayIds = existingRelayIds, + onRelayAdded = { withBGApi { setGroupMembers(rhId, groupInfo, chatModel) } }, + close = close + ) + } + }, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Icon( + painterResource(MR.images.ic_add), + contentDescription = null, + tint = MaterialTheme.colors.primary + ) + Spacer(Modifier.width(4.dp)) + Text( + generalGetString(MR.strings.add_relay_button), + color = MaterialTheme.colors.primary + ) + } + } + } + */ SectionBottomSpacer() } } @@ -131,7 +179,9 @@ private fun subscriberRelayStatusText(member: GroupMember): String { } private fun ownerRelayStatusText(member: GroupMember, groupRelays: List): String { - return if (member.activeConn?.connStatus is ConnStatus.Failed) { + return if (member.memberStatus in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted)) { + relayConnStatus(member).first + } else if (member.activeConn?.connStatus is ConnStatus.Failed) { generalGetString(MR.strings.relay_conn_status_failed) } else if (member.activeConn?.connDisabled == true) { generalGetString(MR.strings.member_info_member_disabled) @@ -144,6 +194,11 @@ private fun ownerRelayStatusText(member: GroupMember, groupRelays: List { + when (member.memberStatus) { + GroupMemberStatus.MemLeft -> return generalGetString(MR.strings.relay_conn_status_removed_by_operator) to Color.Red + GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted -> return member.memberStatus.text to Color.Red + else -> {} + } return when (member.activeConn?.connStatus) { is ConnStatus.Ready -> generalGetString(MR.strings.relay_conn_status_connected) to Color.Green is ConnStatus.Deleted -> generalGetString(MR.strings.relay_conn_status_deleted) to Color.Red diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 78eb31ccbe..0f64479359 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -167,7 +167,7 @@ fun ModalData.GroupChatInfoView( clearChat = { clearChatDialog(chat, close) }, leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) }, manageGroupLink = { - ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated, isChannel = groupInfo.useRelays) } + ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated, isChannel = groupInfo.useRelays, shareGroupInfo = groupInfo) } }, onSearchClicked = onSearchClicked, deletingItems = deletingItems @@ -239,39 +239,88 @@ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl ) } -private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMember) { - val titleId = if (groupInfo.useRelays) MR.strings.button_remove_subscriber_question - else MR.strings.button_remove_member_question - val messageId = if (groupInfo.useRelays) - MR.strings.subscriber_will_be_removed_from_channel_cannot_be_undone - else if (groupInfo.businessChat == null) - MR.strings.member_will_be_removed_from_group_cannot_be_undone - else - MR.strings.member_will_be_removed_from_chat_cannot_be_undone - AlertManager.shared.showAlertDialogButtonsColumn( - generalGetString(titleId), - generalGetString(messageId), - buttons = { - Column { - SectionItemView({ - AlertManager.shared.hideAlert() - removeMembers(rhId, groupInfo, listOf(mem.groupMemberId), withMessages = false) - }) { - Text(generalGetString(MR.strings.remove_member_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) +fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMember) { + if (mem.memberRole == GroupMemberRole.Relay) { + val isLastActive = groupInfo.useRelays && mem.memberCurrent && run { + val activeRelays = ChatModel.groupMembers.value.filter { it.memberRole == GroupMemberRole.Relay && it.memberCurrent } + activeRelays.size <= 1 + } + val message = if (isLastActive) generalGetString(MR.strings.last_active_relay_warning) + else generalGetString(MR.strings.relay_will_be_removed_from_channel) + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.button_remove_relay_question), + message, + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + removeMembers(rhId, groupInfo, listOf(mem.groupMemberId), withMessages = false) + }) { + Text(generalGetString(MR.strings.remove_member_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } } - SectionItemView({ - AlertManager.shared.hideAlert() - removeMembers(rhId, groupInfo, listOf(mem.groupMemberId), withMessages = true) - }) { - Text(generalGetString(MR.strings.remove_member_delete_messages_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + }) + } else if (groupInfo.useRelays) { + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.button_remove_subscriber_question), + generalGetString(MR.strings.subscriber_will_be_removed_from_channel_cannot_be_undone), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + removeMembers(rhId, groupInfo, listOf(mem.groupMemberId), withMessages = false) + }) { + Text(generalGetString(MR.strings.remove_member_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + removeMembers(rhId, groupInfo, listOf(mem.groupMemberId), withMessages = true) + }) { + Text(generalGetString(MR.strings.remove_member_delete_messages_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } } - SectionItemView({ - AlertManager.shared.hideAlert() - }) { - Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + }) + } else { + val titleId = MR.strings.button_remove_member_question + val messageId = if (groupInfo.businessChat == null) + MR.strings.member_will_be_removed_from_group_cannot_be_undone + else + MR.strings.member_will_be_removed_from_chat_cannot_be_undone + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(titleId), + generalGetString(messageId), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + removeMembers(rhId, groupInfo, listOf(mem.groupMemberId), withMessages = false) + }) { + Text(generalGetString(MR.strings.remove_member_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + removeMembers(rhId, groupInfo, listOf(mem.groupMemberId), withMessages = true) + }) { + Text(generalGetString(MR.strings.remove_member_delete_messages_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } } - } - }) + }) + } } private fun removeMembersAlert(rhId: Long?, groupInfo: GroupInfo, memberIds: List, onSuccess: () -> Unit = {}) { @@ -546,6 +595,10 @@ fun ModalData.GroupChatInfoLayout( var anyTopSectionRowShow = false val channelLink = groupInfo.groupProfile.publicGroup?.groupLink + val showUserSupportChat = groupInfo.membership.memberActive && + ((groupInfo.fullGroupPreferences.support.on && groupInfo.membership.memberRole < GroupMemberRole.Moderator) + || groupInfo.membership.supportChat != null) + if (groupInfo.useRelays) { SectionView { if (groupInfo.isOwner && groupLink != null) { @@ -554,11 +607,24 @@ fun ModalData.GroupChatInfoLayout( } else if (channelLink != null) { anyTopSectionRowShow = true ChannelLinkQRCodeSection(channelLink) + ShareViaChatButton { + chatModel.sharedContent.value = SharedContent.ChatLink(groupInfo) + chatModel.chatId.value = null + ModalManager.closeAllModalsEverywhere() + } } if (groupInfo.isOwner || activeSortedMembers.any { it.memberRole >= GroupMemberRole.Owner }) { anyTopSectionRowShow = true ChannelMembersButton(chat.remoteHostId, groupInfo, showMemberInfo) } + if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + anyTopSectionRowShow = true + MemberSupportButton(chat, openMemberSupport) + } + if (showUserSupportChat) { + anyTopSectionRowShow = true + UserSupportChatButton(chat, groupInfo, scrollToItemId) + } } if (!groupInfo.isOwner && channelLink != null) { SectionTextFooter(stringResource(MR.strings.you_can_share_channel_link_anybody_will_be_able_to_connect)) @@ -585,41 +651,33 @@ fun ModalData.GroupChatInfoLayout( } } } - if ( - groupInfo.membership.memberActive && - (groupInfo.membership.memberRole < GroupMemberRole.Moderator || groupInfo.membership.supportChat != null) - ) { + if (showUserSupportChat) { anyTopSectionRowShow = true UserSupportChatButton(chat, groupInfo, scrollToItemId) } } } - val showEditSection = (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) - || groupInfo.groupProfile.description != null - || !groupInfo.useRelays if (anyTopSectionRowShow) { SectionDividerSpaced(maxBottomPadding = false) } - if (showEditSection) { - SectionView { - if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) { - val editProfileTitleId = if (groupInfo.useRelays) MR.strings.button_edit_channel_profile else MR.strings.button_edit_group_profile - EditGroupProfileButton(editProfileTitleId, editGroupProfile) - } - if (groupInfo.groupProfile.description != null || (groupInfo.isOwner && groupInfo.businessChat?.chatType == null)) { - AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage) - } - if (!groupInfo.useRelays) { - val prefsTitleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences - GroupPreferencesButton(prefsTitleId, openPreferences) - } + SectionView { + if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) { + val editProfileTitleId = if (groupInfo.useRelays) MR.strings.button_edit_channel_profile else MR.strings.button_edit_group_profile + EditGroupProfileButton(editProfileTitleId, editGroupProfile) } - if (!groupInfo.useRelays) { - val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs - SectionTextFooter(stringResource(footerId)) + if (groupInfo.groupProfile.description != null || (groupInfo.isOwner && groupInfo.businessChat?.chatType == null)) { + AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage) } - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + val prefsTitleId = if (groupInfo.useRelays) MR.strings.channel_preferences + else if (groupInfo.businessChat == null) MR.strings.group_preferences + else MR.strings.chat_preferences + GroupPreferencesButton(prefsTitleId, openPreferences) } + val footerId = if (groupInfo.useRelays) MR.strings.only_channel_owners_can_change_prefs + else if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs + else MR.strings.only_chat_owners_can_change_prefs + SectionTextFooter(stringResource(footerId)) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) SectionView { if (!groupInfo.useRelays) { @@ -1138,6 +1196,15 @@ private fun ChannelLinkQRCodeSection(groupLink: String) { } } +@Composable +private fun ShareViaChatButton(onClick: () -> Unit) { + SectionItemView(onClick) { + Icon(painterResource(MR.images.ic_forward), null, tint = MaterialTheme.colors.primary) + Spacer(Modifier.width(8.dp)) + Text(stringResource(MR.strings.share_via_chat), color = MaterialTheme.colors.primary) + } +} + @Composable private fun ChannelMembersButton(rhId: Long?, groupInfo: GroupInfo, showMemberInfo: (GroupMember, GroupRelay?) -> Unit) { val title = if (groupInfo.isOwner) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index c9745359b9..57ba0fbd88 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -33,6 +33,7 @@ fun GroupLinkView( onGroupLinkUpdated: ((GroupLink?) -> Unit)?, creatingGroup: Boolean = false, isChannel: Boolean = false, + shareGroupInfo: GroupInfo? = null, close: (() -> Unit)? = null ) { var groupLinkVar by rememberSaveable(stateSaver = GroupLink.nullableStateSaver) { mutableStateOf(groupLink) } @@ -124,6 +125,7 @@ fun GroupLinkView( groupLinkMemberRole, creatingLink, isChannel = isChannel, + shareGroupInfo = shareGroupInfo, createLink = ::createLink, showAddShortLinkAlert = ::showAddShortLinkAlert, updateLink = { @@ -171,6 +173,7 @@ fun GroupLinkLayout( groupLinkMemberRole: MutableState, creatingLink: Boolean, isChannel: Boolean = false, + shareGroupInfo: GroupInfo? = null, createLink: () -> Unit, showAddShortLinkAlert: ((() -> Unit)?) -> Unit, updateLink: () -> Unit, @@ -230,40 +233,61 @@ fun GroupLinkLayout( } else null) { SimpleXCreatedLinkQRCode(groupLink.connLinkContact, short = showShortLink.value) } - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 10.dp) - ) { - val clipboard = LocalClipboardManager.current - SimpleButton( - stringResource(MR.strings.share_link), - icon = painterResource(MR.images.ic_share), - click = { - if (!isChannel && groupLink.shouldBeUpgraded) { - showAddShortLinkAlert { - clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value)) - } - } else { + if (!isChannel && groupLink.shouldBeUpgraded) { + SettingsActionItem( + painterResource(MR.images.ic_add), + stringResource(MR.strings.upgrade_group_link), + click = { showAddShortLinkAlert(null) }, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) + } + val clipboard = LocalClipboardManager.current + SettingsActionItem( + painterResource(MR.images.ic_share), + stringResource(MR.strings.share_link), + click = { + if (!isChannel && groupLink.shouldBeUpgraded) { + showAddShortLinkAlert { clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value)) } + } else { + clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value)) } + }, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) + if (shareGroupInfo != null) { + SettingsActionItem( + painterResource(MR.images.ic_forward), + stringResource(MR.strings.share_via_chat), + click = { + chatModel.sharedContent.value = SharedContent.ChatLink(shareGroupInfo) + chatModel.chatId.value = null + ModalManager.closeAllModalsEverywhere() + }, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, ) - if (creatingGroup && close != null) { - ContinueButton(close) - } else if (!isChannel) { - SimpleButton( - stringResource(MR.strings.delete_link), - icon = painterResource(MR.images.ic_delete), - color = Color.Red, - click = deleteLink - ) - } } - if (!isChannel && groupLink.shouldBeUpgraded) { - AddShortLinkButton(text = stringResource(MR.strings.upgrade_group_link)) { - showAddShortLinkAlert(null) - } + if (!creatingGroup && !isChannel) { + SettingsActionItem( + painterResource(MR.images.ic_delete), + stringResource(MR.strings.delete_link), + click = deleteLink, + iconColor = Color.Red, + textColor = Color.Red, + ) + } + if (creatingGroup && close != null) { + SettingsActionItem( + painterResource(MR.images.ic_check), + stringResource(MR.strings.continue_to_next_step), + click = close, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) } } } @@ -271,17 +295,6 @@ fun GroupLinkLayout( } } -@Composable -private fun AddShortLinkButton(text: String, onClick: () -> Unit) { - SettingsActionItem( - painterResource(MR.images.ic_add), - text, - onClick, - iconColor = MaterialTheme.colors.primary, - textColor = MaterialTheme.colors.primary, - ) -} - @Composable private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState, enabled: Boolean = true) { Row( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index fc5d697f4f..28cbb663a6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -242,34 +242,86 @@ fun GroupMemberInfoView( } fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) { - val messageId = if (groupInfo.businessChat == null) - MR.strings.member_will_be_removed_from_group_cannot_be_undone - else - MR.strings.member_will_be_removed_from_chat_cannot_be_undone - AlertManager.shared.showAlertDialogButtonsColumn( - generalGetString(MR.strings.button_remove_member_question), - generalGetString(messageId), - buttons = { - Column { - SectionItemView({ - AlertManager.shared.hideAlert() - removeMember(rhId, groupInfo, member, withMessages = false, chatModel, close) - }) { - Text(generalGetString(MR.strings.remove_member_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + if (member.memberRole == GroupMemberRole.Relay) { + val isLastActive = groupInfo.useRelays && run { + val activeRelays = chatModel.groupMembers.value.filter { it.memberRole == GroupMemberRole.Relay && it.memberCurrent } + activeRelays.size <= 1 + } + val message = if (isLastActive) generalGetString(MR.strings.last_active_relay_warning) + else generalGetString(MR.strings.relay_will_be_removed_from_channel) + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.button_remove_relay_question), + message, + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + removeMember(rhId, groupInfo, member, withMessages = false, chatModel, close) + }) { + Text(generalGetString(MR.strings.remove_member_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } } - SectionItemView({ - AlertManager.shared.hideAlert() - removeMember(rhId, groupInfo, member, withMessages = true, chatModel, close) - }) { - Text(generalGetString(MR.strings.remove_member_delete_messages_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + }) + } else if (groupInfo.useRelays) { + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.button_remove_subscriber_question), + generalGetString(MR.strings.subscriber_will_be_removed_from_channel_cannot_be_undone), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + removeMember(rhId, groupInfo, member, withMessages = false, chatModel, close) + }) { + Text(generalGetString(MR.strings.remove_member_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + removeMember(rhId, groupInfo, member, withMessages = true, chatModel, close) + }) { + Text(generalGetString(MR.strings.remove_member_delete_messages_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } } - SectionItemView({ - AlertManager.shared.hideAlert() - }) { - Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + }) + } else { + val messageId = if (groupInfo.businessChat == null) + MR.strings.member_will_be_removed_from_group_cannot_be_undone + else + MR.strings.member_will_be_removed_from_chat_cannot_be_undone + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.button_remove_member_question), + generalGetString(messageId), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + removeMember(rhId, groupInfo, member, withMessages = false, chatModel, close) + }) { + Text(generalGetString(MR.strings.remove_member_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + removeMember(rhId, groupInfo, member, withMessages = true, chatModel, close) + }) { + Text(generalGetString(MR.strings.remove_member_delete_messages_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } } - } - }) + }) + } } fun deleteMemberMessagesDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) { @@ -368,6 +420,7 @@ fun GroupMemberInfoLayout( @Composable fun ModeratorDestructiveSection() { val canBlockForAll = member.canBlockForAll(groupInfo) + // TODO [relays] re-enable when relay management ships val canRemove = member.canBeRemoved(groupInfo) && member.memberRole != GroupMemberRole.Relay if (canBlockForAll || canRemove) { SectionDividerSpaced(maxBottomPadding = false) @@ -380,10 +433,10 @@ fun GroupMemberInfoLayout( } } if (canRemove) { - if (member.memberStatus == GroupMemberStatus.MemRemoved || member.memberStatus == GroupMemberStatus.MemLeft) { + if (member.memberStatus != GroupMemberStatus.MemRemoved && (member.memberStatus != GroupMemberStatus.MemLeft || member.memberRole == GroupMemberRole.Relay)) { + RemoveMemberButton(groupInfo.useRelays, member.memberRole == GroupMemberRole.Relay, removeMember) + } else if (member.memberRole != GroupMemberRole.Relay) { DeleteMemberMessagesButton(deleteMemberMessages) - } else { - RemoveMemberButton(groupInfo.useRelays, removeMember) } } } @@ -483,14 +536,15 @@ fun GroupMemberInfoLayout( SectionSpacer() } + val showMemberSupportChat = !openedFromSupportChat && + groupInfo.membership.memberRole >= GroupMemberRole.Moderator && + member.memberRole != GroupMemberRole.Relay && + ((groupInfo.fullGroupPreferences.support.on && member.memberRole < GroupMemberRole.Moderator) + || member.supportChat != null) + if (member.memberActive) { SectionView { - if ( - !openedFromSupportChat && - groupInfo.membership.memberRole >= GroupMemberRole.Moderator && - member.memberRole != GroupMemberRole.Relay && - (member.memberRole < GroupMemberRole.Moderator || member.supportChat != null) - ) { + if (showMemberSupportChat) { SupportChatButton() } if (connectionCode != null && !(groupInfo.useRelays && member.memberRole == GroupMemberRole.Relay)) { @@ -504,6 +558,11 @@ fun GroupMemberInfoLayout( // } } SectionDividerSpaced() + } else if (groupInfo.useRelays && member.memberCurrent && showMemberSupportChat) { + SectionView { + SupportChatButton() + } + SectionDividerSpaced() } if (member.contactLink != null) { @@ -747,8 +806,10 @@ fun UnblockForAllButton(onClick: () -> Unit) { } @Composable -fun RemoveMemberButton(useRelays: Boolean = false, onClick: () -> Unit) { - val label = if (useRelays) MR.strings.button_remove_subscriber else MR.strings.button_remove_member +fun RemoveMemberButton(useRelays: Boolean = false, isRelay: Boolean = false, onClick: () -> Unit) { + val label = if (isRelay) MR.strings.button_remove_relay + else if (useRelays) MR.strings.button_remove_subscriber + else MR.strings.button_remove_member SettingsActionItem( painterResource(MR.images.ic_delete), stringResource(label), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt index b8db5969a1..740349eaea 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt @@ -10,6 +10,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable +import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -43,7 +44,7 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> fun savePrefs(afterSave: () -> Unit = {}) { withBGApi { val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences()) - val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp) + val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp, gInfo.useRelays) if (g != null) { withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId, g) @@ -56,10 +57,12 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> afterSave() } } + val saveTextId = if (gInfo.useRelays) MR.strings.save_and_notify_channel_subscribers + else MR.strings.save_and_notify_group_members ModalView( close = { if (preferences == currentPreferences) close() - else showUnsavedChangesAlert({ savePrefs(close) }, close) + else showUnsavedChangesAlert({ savePrefs(close) }, close, saveTextId) }, ) { GroupPreferencesLayout( @@ -97,17 +100,11 @@ private fun GroupPreferencesLayout( savePrefs: () -> Unit, openMemberAdmission: () -> Unit, ) { - ColumnWithScrollBar { - val titleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences - AppBarTitle(stringResource(titleId)) - if (groupInfo.businessChat == null) { - MemberAdmissionButton(openMemberAdmission) - SectionDividerSpaced(maxBottomPadding = false) - } + val onTTLUpdated = { ttl: Int? -> + applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl))) + } + @Composable fun TimedMessagesPreference() { val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) } - val onTTLUpdated = { ttl: Int? -> - applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl))) - } FeatureSection(GroupFeature.TimedMessages, timedMessages, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> if (enable == GroupFeatureEnabled.ON) { applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = preferences.timedMessages.ttl ?: 86400))) @@ -115,58 +112,127 @@ private fun GroupPreferencesLayout( applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = currentPreferences.timedMessages.ttl))) } } - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun DirectMessagesPreference() { val allowDirectMessages = remember(preferences) { mutableStateOf(preferences.directMessages.enable) } val directMessagesRole = remember(preferences) { mutableStateOf(preferences.directMessages.role) } FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, directMessagesRole, groupInfo, preferences, onTTLUpdated) { enable, role -> applyPrefs(preferences.copy(directMessages = RoleGroupPreference(enable = enable, role))) } - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun FullDeletePreference() { val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.enable) } FeatureSection(GroupFeature.FullDelete, allowFullDeletion, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = enable))) } - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun ReactionsPreference() { val allowReactions = remember(preferences) { mutableStateOf(preferences.reactions.enable) } FeatureSection(GroupFeature.Reactions, allowReactions, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> applyPrefs(preferences.copy(reactions = GroupPreference(enable = enable))) } - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun VoicePreference() { val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) } val voiceRole = remember(preferences) { mutableStateOf(preferences.voice.role) } FeatureSection(GroupFeature.Voice, allowVoice, voiceRole, groupInfo, preferences, onTTLUpdated) { enable, role -> applyPrefs(preferences.copy(voice = RoleGroupPreference(enable = enable, role))) } - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun FilesPreference() { val allowFiles = remember(preferences) { mutableStateOf(preferences.files.enable) } val filesRole = remember(preferences) { mutableStateOf(preferences.files.role) } FeatureSection(GroupFeature.Files, allowFiles, filesRole, groupInfo, preferences, onTTLUpdated) { enable, role -> applyPrefs(preferences.copy(files = RoleGroupPreference(enable = enable, role))) } - - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun SimplexLinksPreference() { val allowSimplexLinks = remember(preferences) { mutableStateOf(preferences.simplexLinks.enable) } val simplexLinksRole = remember(preferences) { mutableStateOf(preferences.simplexLinks.role) } FeatureSection(GroupFeature.SimplexLinks, allowSimplexLinks, simplexLinksRole, groupInfo, preferences, onTTLUpdated) { enable, role -> applyPrefs(preferences.copy(simplexLinks = RoleGroupPreference(enable = enable, role))) } - - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun ReportsPreference() { val enableReports = remember(preferences) { mutableStateOf(preferences.reports.enable) } - FeatureSection(GroupFeature.Reports, enableReports, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> + FeatureSection(GroupFeature.Reports, enableReports, null, groupInfo, preferences, onTTLUpdated, disabled = true) { enable, _ -> // enable reports in 7.0 once directory support added applyPrefs(preferences.copy(reports = GroupPreference(enable = enable))) } - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun HistoryPreference() { val enableHistory = remember(preferences) { mutableStateOf(preferences.history.enable) } FeatureSection(GroupFeature.History, enableHistory, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> applyPrefs(preferences.copy(history = GroupPreference(enable = enable))) } + } + @Composable fun SupportPreference(disabled: Boolean = false, notice: String? = null, onEnable: ((() -> Unit) -> Unit)? = null) { + val enableSupport = remember(preferences) { mutableStateOf(preferences.support.enable) } + FeatureSection(GroupFeature.Support, enableSupport, null, groupInfo, preferences, onTTLUpdated, disabled = disabled, notice = notice) { enable, _ -> + applyPrefs(preferences.copy(support = GroupPreference(enable = enable))) + if (enable == GroupFeatureEnabled.ON) onEnable?.invoke { + enableSupport.value = GroupFeatureEnabled.OFF + applyPrefs(preferences.copy(support = GroupPreference(enable = GroupFeatureEnabled.OFF))) + } + } + } + ColumnWithScrollBar { + val titleId = if (groupInfo.useRelays) MR.strings.channel_preferences + else if (groupInfo.businessChat == null) MR.strings.group_preferences + else MR.strings.chat_preferences + AppBarTitle(stringResource(titleId)) + if (!groupInfo.useRelays) { + if (groupInfo.businessChat == null) { + MemberAdmissionButton(openMemberAdmission) + SectionDividerSpaced(maxBottomPadding = false) + } + TimedMessagesPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + DirectMessagesPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + FullDeletePreference() + SectionDividerSpaced(true, maxBottomPadding = false) + ReactionsPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + VoicePreference() + SectionDividerSpaced(true, maxBottomPadding = false) + FilesPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + SimplexLinksPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + ReportsPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + HistoryPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + SupportPreference(disabled = true) + } else { + TimedMessagesPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + FullDeletePreference() + SectionDividerSpaced(true, maxBottomPadding = false) + ReactionsPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + HistoryPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + SupportPreference(notice = generalGetString(MR.strings.chat_with_admins_relay_note), onEnable = { revert -> + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.enable_chats_with_admins_question), + text = generalGetString(MR.strings.chat_with_admins_relay_note), + confirmText = generalGetString(MR.strings.enable_chats_with_admins), + destructive = true, + onDismiss = revert, + onDismissRequest = revert, + ) + }) + } if (groupInfo.isOwner) { SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + val saveTextId = if (groupInfo.useRelays) MR.strings.save_and_notify_channel_subscribers + else MR.strings.save_and_notify_group_members ResetSaveButtons( reset = reset, save = savePrefs, - disabled = preferences == currentPreferences + disabled = preferences == currentPreferences, + saveTextId = saveTextId ) } SectionBottomSpacer() @@ -190,6 +256,8 @@ private fun FeatureSection( groupInfo: GroupInfo, preferences: FullGroupPreferences, onTTLUpdated: (Int?) -> Unit, + disabled: Boolean = false, + notice: String? = null, onSelected: (GroupFeatureEnabled, GroupMemberRole?) -> Unit ) { SectionView { @@ -199,10 +267,10 @@ private fun FeatureSection( val timedOn = feature == GroupFeature.TimedMessages && enableFeature.value == GroupFeatureEnabled.ON if (groupInfo.isOwner) { PreferenceToggleWithIcon( - feature.text, + feature.text(groupInfo.isChannel), icon, iconTint, - disabled = feature == GroupFeature.Reports, // remove in 6.4 + disabled = disabled, checked = enableFeature.value == GroupFeatureEnabled.ON, ) { checked -> onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF, enableForRole?.value) @@ -231,7 +299,7 @@ private fun FeatureSection( } } else { InfoRow( - feature.text, + feature.text(groupInfo.isChannel), enableFeature.value.text, icon = icon, iconTint = iconTint, @@ -249,25 +317,28 @@ private fun FeatureSection( onSelected(enableFeature.value, null) } } - SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.isOwner)) + SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.isOwner, groupInfo.isChannel)) + if (notice != null) { + SectionTextFooter(notice) + } } @Composable -private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) { +private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean, saveTextId: StringResource) { SectionView { SectionItemView(reset, disabled = disabled) { Text(stringResource(MR.strings.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) } SectionItemView(save, disabled = disabled) { - Text(stringResource(MR.strings.save_and_notify_group_members), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + Text(stringResource(saveTextId), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) } } } -private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { +private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit, confirmTextId: StringResource) { AlertManager.shared.showAlertDialogStacked( title = generalGetString(MR.strings.save_preferences_question), - confirmText = generalGetString(MR.strings.save_and_notify_group_members), + confirmText = generalGetString(confirmTextId), dismissText = generalGetString(MR.strings.exit_without_saving), onConfirm = save, onDismiss = revert, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt index f15f70673a..6e91ad92d6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt @@ -32,10 +32,11 @@ import java.net.URI fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) { GroupProfileLayout( close = close, + groupInfo = groupInfo, groupProfile = groupInfo.groupProfile, saveProfile = { p -> withBGApi { - val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p) + val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p, groupInfo.useRelays) if (gInfo != null) { withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId, gInfo) @@ -50,9 +51,11 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl @Composable fun GroupProfileLayout( close: () -> Unit, + groupInfo: GroupInfo, groupProfile: GroupProfile, saveProfile: (GroupProfile) -> Unit, ) { + val isChannel = groupInfo.useRelays val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val displayName = rememberSaveable { mutableStateOf(groupProfile.displayName) } val fullName = rememberSaveable { mutableStateOf(groupProfile.fullName) } @@ -71,7 +74,7 @@ fun GroupProfileLayout( if (dataUnchanged || !canUpdateProfile(displayName.value, shortDescr.value, groupProfile)) { close() } else { - showUnsavedChangesAlert({ + showUnsavedChangesAlert(isChannel, { saveProfile( groupProfile.copy( displayName = displayName.value.trim(), @@ -103,7 +106,11 @@ fun GroupProfileLayout( Modifier.fillMaxWidth() .padding(horizontal = DEFAULT_PADDING) ) { - ReadableText(MR.strings.group_profile_is_stored_on_members_devices, TextAlign.Center) + ReadableText( + if (isChannel) MR.strings.channel_profile_is_stored_on_subscribers_devices + else MR.strings.group_profile_is_stored_on_members_devices, + TextAlign.Center + ) Box( Modifier .fillMaxWidth() @@ -112,7 +119,7 @@ fun GroupProfileLayout( ) { Box(contentAlignment = Alignment.TopEnd) { Box(contentAlignment = Alignment.Center) { - ProfileImage(108.dp, profileImage.value, color = MaterialTheme.colors.secondary.copy(alpha = 0.1f)) + ProfileImage(108.dp, profileImage.value, icon = groupInfo.chatIconName, color = MaterialTheme.colors.secondary.copy(alpha = 0.1f)) EditImageButton { scope.launch { bottomSheetModalState.show() } } } if (profileImage.value != null) { @@ -122,7 +129,7 @@ fun GroupProfileLayout( } Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( - stringResource(MR.strings.group_display_name_field), + stringResource(if (isChannel) MR.strings.channel_display_name_field else MR.strings.group_display_name_field), fontSize = 16.sp ) if (!isValidNewProfileName(displayName.value, groupProfile)) { @@ -136,7 +143,7 @@ fun GroupProfileLayout( if (groupProfile.fullName.trim().isNotEmpty() && groupProfile.fullName.trim() != groupProfile.displayName.trim()) { Spacer(Modifier.height(DEFAULT_PADDING)) Text( - stringResource(MR.strings.group_full_name_field), + stringResource(if (isChannel) MR.strings.channel_full_name_field else MR.strings.group_full_name_field), fontSize = 16.sp, modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) ) @@ -164,9 +171,10 @@ fun GroupProfileLayout( Spacer(Modifier.height(DEFAULT_PADDING)) val enabled = !dataUnchanged && canUpdateProfile(displayName.value, shortDescr.value, groupProfile) + val saveProfileLabel = if (isChannel) MR.strings.save_channel_profile else MR.strings.save_group_profile if (enabled) { Text( - stringResource(MR.strings.save_group_profile), + stringResource(saveProfileLabel), modifier = Modifier.clickable { saveProfile( groupProfile.copy( @@ -181,7 +189,7 @@ fun GroupProfileLayout( ) } else { Text( - stringResource(MR.strings.save_group_profile), + stringResource(saveProfileLabel), color = MaterialTheme.colors.secondary ) } @@ -204,10 +212,10 @@ private fun canUpdateProfile(displayName: String, shortDescr: String, groupProfi private fun isValidNewProfileName(displayName: String, groupProfile: GroupProfile): Boolean = displayName == groupProfile.displayName || isValidDisplayName(displayName.trim()) -private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { +private fun showUnsavedChangesAlert(isChannel: Boolean, save: () -> Unit, revert: () -> Unit) { AlertManager.shared.showAlertDialogStacked( title = generalGetString(MR.strings.save_preferences_question), - confirmText = generalGetString(MR.strings.save_and_notify_group_members), + confirmText = generalGetString(if (isChannel) MR.strings.save_and_notify_channel_subscribers else MR.strings.save_and_notify_group_members), dismissText = generalGetString(MR.strings.exit_without_saving), onConfirm = save, onDismiss = revert, @@ -224,6 +232,7 @@ fun PreviewGroupProfileLayout() { SimpleXTheme { GroupProfileLayout( close = {}, + groupInfo = GroupInfo.sampleData, groupProfile = GroupProfile.sampleData, saveProfile = { _ -> } ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt index 48171bfeb7..7c9db58316 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt @@ -34,7 +34,7 @@ fun MemberAdmissionView(m: ChatModel, rhId: Long?, chatId: String, close: () -> fun saveAdmission(afterSave: () -> Unit = {}) { withBGApi { val gp = gInfo.groupProfile.copy(memberAdmission = admission) - val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp) + val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp, gInfo.useRelays) if (g != null) { withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId, g) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt index c3cf954ab6..3d76c845ad 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt @@ -116,7 +116,10 @@ private fun ModalData.MemberSupportViewLayout( if (membersWithChats.isEmpty()) { item { Box(Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING), contentAlignment = Alignment.Center) { - Text(generalGetString(MR.strings.no_support_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) + Text( + generalGetString(if (groupInfo.fullGroupPreferences.support.on) MR.strings.no_support_chats else MR.strings.support_chats_disabled), + color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center + ) } } } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt index 1e99c7f527..927e9940b5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt @@ -45,7 +45,7 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () welcome = null } val groupProfileUpdated = gInfo.groupProfile.copy(description = welcome) - val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated) + val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated, gInfo.useRelays) if (res != null) { gInfo = res withContext(Dispatchers.Main) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatLinkHeader.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatLinkHeader.kt new file mode 100644 index 0000000000..3c3e4baf49 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatLinkHeader.kt @@ -0,0 +1,79 @@ +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.Modifier +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.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.ProfileImage +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.stringResource + +@Composable +fun CIChatLinkHeader( + chatLink: MsgChatLink, + ownerSig: LinkOwnerSig?, + hasText: Boolean, +) { + Column( + Modifier + .defaultMinSize(minWidth = 220.dp) + .padding(start = 8.dp, end = 12.dp, top = 8.dp, bottom = 4.dp) + ) { + Row( + Modifier.defaultMinSize(minWidth = 220.dp) + ) { + ProfileImage( + size = 54.dp, + image = chatLink.image, + icon = chatLink.iconRes, + color = if (isInDarkTheme()) FileDark else FileLight + ) + Spacer(Modifier.width(8.dp)) + Column( + Modifier.defaultMinSize(minHeight = 54.dp), + verticalArrangement = Arrangement.Center + ) { + Text( + chatLink.displayName, + style = MaterialTheme.typography.caption, + fontWeight = FontWeight.Medium, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + val fn = chatLink.fullName + if (fn.isNotEmpty() && fn != chatLink.displayName) { + Text(fn, maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + Divider(Modifier.fillMaxWidth().padding(top = 8.dp)) + Column(Modifier.padding(top = 8.dp, bottom = 4.dp, start = 4.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) { + chatLink.shortDescription?.let { descr -> + Text( + descr, + color = MaterialTheme.colors.secondary, + fontSize = 13.sp, + lineHeight = 18.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + Text( + chatLink.infoLine(signed = ownerSig != null), + color = MaterialTheme.colors.secondary, + fontSize = 13.sp, + lineHeight = 18.sp, + ) + Text( + stringResource(MR.strings.tap_to_open), + color = MaterialTheme.colors.primary, + ) + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index 542623028a..afd55ed928 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -127,7 +127,8 @@ fun CIFileView( fun fileIndicator() { Box( Modifier - .size(42.sp.toDp() * sizeMultiplier) + .padding(top = 2.sp.toDp()) + .size(40.sp.toDp() * sizeMultiplier) .clip(RoundedCornerShape(4.sp.toDp() * sizeMultiplier)), contentAlignment = Alignment.Center ) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt index 39bb9545e1..9b8393f66a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* @@ -52,15 +53,12 @@ fun CIGroupInvitationView( else if (isInDarkTheme()) FileDark else FileLight Row( - Modifier - .defaultMinSize(minWidth = 220.dp) - .padding(vertical = 4.dp) - .padding(end = 2.dp) + Modifier.defaultMinSize(minWidth = 220.dp) ) { - ProfileImage(size = 60.dp, image = groupInvitation.groupProfile.image, icon = MR.images.ic_supervised_user_circle_filled, color = iconColor) - Spacer(Modifier.padding(horizontal = 3.dp)) + ProfileImage(size = 54.dp, image = groupInvitation.groupProfile.image, icon = MR.images.ic_supervised_user_circle_filled, color = iconColor) + Spacer(Modifier.width(8.dp)) Column( - Modifier.defaultMinSize(minHeight = 60.dp), + Modifier.defaultMinSize(minHeight = 54.dp), verticalArrangement = Arrangement.Center ) { Text(p.displayName, style = MaterialTheme.typography.caption, fontWeight = FontWeight.Medium, maxLines = 2, overflow = TextOverflow.Ellipsis) @@ -98,8 +96,7 @@ fun CIGroupInvitationView( Box( Modifier .width(IntrinsicSize.Min) - .padding(vertical = 3.dp) - .padding(start = 8.dp, end = 12.dp), + .padding(start = 8.dp, end = 12.dp, top = 8.dp, bottom = 4.dp), contentAlignment = Alignment.BottomEnd ) { Box( @@ -112,10 +109,10 @@ fun CIGroupInvitationView( ) { groupInfoView() val secondaryColor = MaterialTheme.colors.secondary - Column(Modifier.padding(top = 2.dp, start = 5.dp)) { - Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp)) + Divider(Modifier.fillMaxWidth().padding(top = 8.dp)) + Column(Modifier.padding(top = 8.dp, bottom = 4.dp, start = 4.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) { if (action) { - Text(groupInvitationStr()) + Text(groupInvitationStr(), fontSize = 13.sp, lineHeight = 18.sp) Text( buildAnnotatedString { append(generalGetString(if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join)) @@ -131,7 +128,9 @@ fun CIGroupInvitationView( buildAnnotatedString { append(groupInvitationStr()) withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) } - } + }, + fontSize = 13.sp, + lineHeight = 18.sp, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 05c84db4c3..15b0a12822 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -48,8 +48,8 @@ private val msgTailMaxHeightDp = msgTailWidthDp * 1.732f // 60deg 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(ci: ChatItem, isChannel: Boolean = false): AnnotatedString = + chatEventText(ci.content.text(isChannel), ci.timestampText) fun chatEventText(eventText: String, ts: String): AnnotatedString = buildAnnotatedString { @@ -402,7 +402,7 @@ fun ChatItemView( if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) { MsgReactionsMenu() } - if (cItem.meta.itemDeleted == null && !live && !cItem.localNote) { + if (cItem.meta.itemDeleted == null && !live && !cItem.localNote && cInfo.sendMsgEnabled) { ItemAction(stringResource(MR.strings.reply_verb), painterResource(MR.images.ic_reply), onClick = { if (composeState.value.editing) { composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) @@ -612,7 +612,7 @@ fun ChatItemView( return if (count <= 1) { null } else if (ns.isEmpty()) { - generalGetString(MR.strings.rcv_group_events_count).format(count) + generalGetString(if (cInfo.isChannel) MR.strings.rcv_channel_events_count else MR.strings.rcv_group_events_count).format(count) } else if (count > ns.size) { members + " " + generalGetString(MR.strings.rcv_group_and_other_events).format(count - ns.size) } else { @@ -629,9 +629,9 @@ fun ChatItemView( buildAnnotatedString { withStyle(chatEventStyle) { append(memberDisplayName) } append(" ") - }.plus(chatEventText(cItem)) + }.plus(chatEventText(cItem, cInfo.isChannel)) } else { - chatEventText(cItem) + chatEventText(cItem, cInfo.isChannel) } } @@ -643,7 +643,7 @@ fun ChatItemView( @Composable fun PendingReviewEventItemView() { Text( buildAnnotatedString { - withStyle(chatEventStyle.copy(fontWeight = FontWeight.Bold)) { append(cItem.content.text) } + withStyle(chatEventStyle.copy(fontWeight = FontWeight.Bold)) { append(cItem.content.text(cInfo.isChannel)) } }, Modifier.padding(horizontal = 6.dp, vertical = 6.dp) ) @@ -680,21 +680,17 @@ fun ChatItemView( } @Composable - fun E2EEInfoNoPQText() { - e2eeInfoText(MR.strings.e2ee_info_no_pq) + fun DirectE2EEInfoText(e2EEInfo: E2EEInfo) { + e2eeInfoText(when (e2EEInfo.pqEnabled) { + true -> MR.strings.e2ee_info_pq + false -> MR.strings.e2ee_info_no_pq + null -> MR.strings.e2ee_info_e2ee + }) } @Composable - fun DirectE2EEInfoText(e2EEInfo: E2EEInfo) { - if (e2EEInfo.pqEnabled != null) { - if (e2EEInfo.pqEnabled) { - e2eeInfoText(MR.strings.e2ee_info_pq) - } else { - E2EEInfoNoPQText() - } - } else { - e2eeInfoText(MR.strings.e2ee_info_e2ee) - } + fun GroupE2EEInfoText(e2EEInfo: E2EEInfo) { + e2eeInfoText(if (e2EEInfo.public == true) MR.strings.e2ee_info_no_e2ee else MR.strings.e2ee_info_no_pq) } if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) { @@ -714,6 +710,9 @@ fun ChatItemView( } else { Box(Modifier.size(0.dp)) {} } + is CIContent.RcvMsgErrorContent -> { + RcvMsgErrorItemView(c.rcvMsgError, cItem, showTimestamp, cInfo.timedMessagesTTL) + } is CIContent.RcvDecryptionError -> { CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember) DeleteItemMenu() @@ -791,8 +790,8 @@ fun ChatItemView( is CIContent.RcvBlocked -> DeletedItem() is CIContent.SndDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo) is CIContent.RcvDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo) - is CIContent.SndGroupE2EEInfo -> E2EEInfoNoPQText() - is CIContent.RcvGroupE2EEInfo -> E2EEInfoNoPQText() + is CIContent.SndGroupE2EEInfo -> GroupE2EEInfoText(c.e2eeInfo) + is CIContent.RcvGroupE2EEInfo -> GroupE2EEInfoText(c.e2eeInfo) is CIContent.ChatBanner -> Spacer(modifier = Modifier.size(0.dp)) is CIContent.InvalidJSON -> { CIInvalidJSONView(c.json) @@ -1310,6 +1309,7 @@ fun shapeStyleWithTail(chatItem: ChatItem? = null, tailEnabled: Boolean, tailVis is CIContent.SndMsgContent, is CIContent.RcvMsgContent, is CIContent.RcvDecryptionError, + is CIContent.RcvMsgErrorContent, is CIContent.SndDeleted, is CIContent.RcvDeleted, is CIContent.RcvIntegrityError, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 8aab0bbbb6..f55c49fdd1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -23,6 +23,7 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.planAndConnect import chat.simplex.res.MR import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -55,7 +56,7 @@ fun FramedItemView( } @Composable - fun ciQuotedMsgTextView(qi: CIQuote, lines: Int, showTimestamp: Boolean) { + fun ciQuotedMsgTextView(qi: CIQuote, lines: Int, showTimestamp: Boolean, stripLink: String? = null, prefix: AnnotatedString? = null) { MarkdownText( qi.text, qi.formattedText, @@ -66,11 +67,13 @@ fun FramedItemView( linkMode = linkMode, uriHandler = if (appPlatform.isDesktop) uriHandler else null, showTimestamp = showTimestamp, + prefix = prefix, + stripLink = stripLink, ) } @Composable - fun ciQuotedMsgView(qi: CIQuote) { + fun ciQuotedMsgView(qi: CIQuote, stripLink: String? = null, prefix: AnnotatedString? = null) { Box( Modifier // this width limitation prevents crash on calculating constraints that may happen if you post veeeery long message and then quote it. @@ -89,10 +92,10 @@ fun FramedItemView( style = TextStyle(fontSize = 13.5.sp, color = if (qi.chatDir is CIDirection.GroupSnd) CurrentColors.value.colors.primary else CurrentColors.value.colors.secondary), maxLines = 1 ) - ciQuotedMsgTextView(qi, lines = 2, showTimestamp = showTimestamp) + ciQuotedMsgTextView(qi, lines = 2, showTimestamp = showTimestamp, stripLink = stripLink, prefix = prefix) } } else { - ciQuotedMsgTextView(qi, lines = 3, showTimestamp = showTimestamp) + ciQuotedMsgTextView(qi, lines = 3, showTimestamp = showTimestamp, stripLink = stripLink, prefix = prefix) } } } @@ -177,6 +180,20 @@ fun FramedItemView( tint = if (isInDarkTheme()) FileDark else FileLight ) } + is MsgContent.MCChat -> { + val prefix = buildAnnotatedString { + append(qi.content.chatLink.displayName + if (qi.content.text != qi.content.chatLink.connLinkStr) " - " else "") + } + Box(Modifier.fillMaxWidth().weight(1f)) { + ciQuotedMsgView(qi, stripLink = qi.content.chatLink.connLinkStr, prefix = prefix) + } + Icon( + painterResource(qi.content.chatLink.smallIconRes), + null, + Modifier.padding(top = 6.dp, end = 4.dp).size(22.dp), + tint = if (isInDarkTheme()) FileDark else FileLight + ) + } else -> ciQuotedMsgView(qi) } } @@ -329,10 +346,26 @@ fun FramedItemView( CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } + is MsgContent.MCChat -> { + val hasText = mc.text != mc.chatLink.connLinkStr + Box( + Modifier.combinedClickable( + onClick = { + withBGApi { planAndConnect(chat.remoteHostId, mc.chatLink.connLinkStr, linkOwnerSig = mc.ownerSig, close = null) } + }, + onLongClick = { showMenu.value = true } + ) + ) { + CIChatLinkHeader(chatLink = mc.chatLink, ownerSig = mc.ownerSig, hasText = hasText) + } + if (hasText) { + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp, stripLink = mc.chatLink.connLinkStr) + } + } is MsgContent.MCReport -> { val prefix = buildAnnotatedString { withStyle(SpanStyle(color = Color.Red, fontStyle = FontStyle.Italic)) { - append(if (mc.text.isEmpty()) mc.reason.text else "${mc.reason.text}: ") + append(itemPrefixText(ci)) } } CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp, prefix = prefix) @@ -366,7 +399,8 @@ fun CIMarkdownText( onLinkLongClick: (link: String) -> Unit = {}, showViaProxy: Boolean, showTimestamp: Boolean, - prefix: AnnotatedString? = null + prefix: AnnotatedString? = null, + stripLink: String? = null ) { val chatInfo = chat.chatInfo val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text @@ -382,6 +416,7 @@ fun CIMarkdownText( else -> null }, uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp, prefix = prefix, + stripLink = stripLink, selectionRange = selection.highlightRange, onTextLayoutResult = selection.onTextLayoutResult ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt index d528396193..08b6520dfa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatItem import chat.simplex.common.model.MsgErrorType +import chat.simplex.common.model.RcvMsgError import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.AlertManager import chat.simplex.common.views.helpers.generalGetString @@ -73,6 +74,19 @@ fun CIMsgError(ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?, onC } } +@Composable +fun RcvMsgErrorItemView(rcvMsgError: RcvMsgError, ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?) { + CIMsgError(ci, showTimestamp, timedMessagesTTL) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.alert_title_msg_error), + text = when (rcvMsgError) { + is RcvMsgError.Dropped -> String.format(generalGetString(MR.strings.alert_text_msg_reception_error), rcvMsgError.attempts) + is RcvMsgError.ParseError -> rcvMsgError.parseError + } + ) + } +} + @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index 9e8583a79b..3358a23e1e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -85,6 +85,13 @@ fun itemDisplayText(ci: ChatItem, linkMode: SimplexLinkMode): String { return formattedText.joinToString("") { itemSegmentDisplayText(it, ci, linkMode) } } +// Display-only prefix rendered before ci.text (e.g. "Spam: " for reports). +// Renderers and selection code MUST share this string — otherwise selection offsets drift from screen. +fun itemPrefixText(ci: ChatItem): String = when (val mc = ci.content.msgContent) { + is MsgContent.MCReport -> if (mc.text.isEmpty()) mc.reason.text else "${mc.reason.text}: " + else -> "" +} + // Text transformations in MarkdownText must match itemSegmentDisplayText above @Composable fun MarkdownText ( @@ -109,9 +116,12 @@ fun MarkdownText ( showViaProxy: Boolean = false, showTimestamp: Boolean = true, prefix: AnnotatedString? = null, + stripLink: String? = null, selectionRange: IntRange? = null, onTextLayoutResult: ((TextLayoutResult) -> Unit)? = null ) { + val text = if (stripLink != null) stripTextLink(text.toString(), stripLink) else text + val formattedText = if (stripLink != null) stripFormattedTextLink(formattedText, stripLink) else formattedText val textLayoutDirection = remember (text) { if (isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr } @@ -299,7 +309,7 @@ fun MarkdownText ( } val clampedRange = selectionRange?.let { it.first .. minOf(it.last, selectableEnd) } if ((hasLinks && uriHandler != null) || hasSecrets || (hasCommands && sendCommandMsg != null)) { - val icon = remember { mutableStateOf(PointerIcon.Default) } + val icon = remember { mutableStateOf(PointerIcon.Text) } ClickableText(annotatedText, style = style, selectionRange = clampedRange, modifier = modifier.pointerHoverIcon(icon.value), maxLines = maxLines, overflow = overflow, onLongClick = { offset -> if (hasLinks) { @@ -336,7 +346,7 @@ fun MarkdownText ( if (hasAnnotation("WEB_URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("OTHER_URL") || hasAnnotation("SECRET") || hasAnnotation("COMMAND")) { PointerIcon.Hand } else { - PointerIcon.Default + PointerIcon.Text } }, shouldConsumeEvent = { offset -> @@ -431,7 +441,7 @@ private fun SelectableText( BasicText( text = text, - modifier = modifier.then(selectionHighlight(selectionRange, text.length, layoutResult)), + modifier = modifier.pointerHoverIcon(PointerIcon.Text).then(selectionHighlight(selectionRange, text.length, layoutResult)), style = style, maxLines = maxLines, overflow = overflow, @@ -532,3 +542,20 @@ private fun isRtl(s: CharSequence): Boolean { } fun mentionText(name: String): String = if (name.contains(" @")) "@'$name'" else "@$name" + +fun stripTextLink(text: String, link: String): String = + if (text == link) "" + else if (text.endsWith("\n$link")) text.dropLast(link.length + 1) + else text + +fun stripFormattedTextLink(ft: List?, link: String): List? { + if (ft == null || ft.isEmpty() || ft.last().text != link) return ft + val result = ft.toMutableList() + result.removeAt(result.lastIndex) + val i = result.lastIndex + if (i >= 0 && result[i].format == null && result[i].text.endsWith("\n")) { + result[i] = FormattedText(result[i].text.dropLast(1), null) + if (result[i].text.isEmpty()) result.removeLast() + } + return result.ifEmpty { null } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index a42f66c6cf..01dcd021f7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -24,7 +24,13 @@ 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.unit.* +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize import chat.simplex.common.AppLock +import chat.simplex.common.BuildConfigCommon import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts @@ -46,7 +52,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.serialization.json.Json import kotlin.time.Duration.Companion.seconds -enum class PresetTagKind { GROUP_REPORTS, FAVORITES, CONTACTS, GROUPS, BUSINESS, NOTES } +enum class PresetTagKind { GROUP_REPORTS, FAVORITES, CONTACTS, GROUPS, CHANNELS, BUSINESS, NOTES } sealed class ActiveFilter { data class PresetTag(val tag: PresetTagKind) : ActiveFilter() @@ -84,41 +90,85 @@ private fun showNewChatSheet(oneHandUI: State) { @Composable fun ToggleChatListCard() { - ChatListCard( - close = { - appPrefs.oneHandUICardShown.set(true) - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.one_hand_ui), - text = generalGetString(MR.strings.one_hand_ui_change_instruction), + val oneHandUI = remember { appPrefs.oneHandUI.state } + val onClose = { + appPrefs.oneHandUICardShown.set(true) + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.one_hand_ui), + text = generalGetString(MR.strings.one_hand_ui_change_instruction), + ) + } + val activeBg = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) + .copy(alpha = appPrefs.inAppBarsAlpha.get()) + val selectedBg = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.92f) + Row( + Modifier + .padding(horizontal = 16.dp, vertical = 12.dp) + .fillMaxWidth() + .height(IntrinsicSize.Min) + .clip(RoundedCornerShape(percent = 50)), + horizontalArrangement = Arrangement.spacedBy(2.dp) + ) { + ToolbarSegment( + icon = MR.images.ic_mobile_3, + text = stringResource(MR.strings.one_hand_ui_bottom_bar), + isSelected = oneHandUI.value, + selectedBg = selectedBg, + activeBg = activeBg, + modifier = Modifier.weight(1f) + ) { appPrefs.oneHandUI.set(true) } + Box(Modifier.weight(1f).fillMaxHeight()) { + ToolbarSegment( + icon = MR.images.ic_mobile_4, + text = stringResource(MR.strings.one_hand_ui_top_bar), + isSelected = !oneHandUI.value, + selectedBg = selectedBg, + activeBg = activeBg, + modifier = Modifier.fillMaxSize() + ) { appPrefs.oneHandUI.set(false) } + Icon( + painterResource(MR.images.ic_close), null, + Modifier + .align(Alignment.CenterEnd) + .padding(end = 4.dp) + .clip(CircleShape) + .clickable(onClick = onClose) + .padding(8.dp) + .size(16.dp), + tint = MaterialTheme.colors.secondary ) } + } +} + +@Composable +private fun ToolbarSegment( + icon: ImageResource, + text: String, + isSelected: Boolean, + selectedBg: Color, + activeBg: Color, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Row( + modifier + .fillMaxHeight() + .background(if (isSelected) selectedBg else activeBg) + .then(if (!isSelected) Modifier.clickable(onClick = onClick) else Modifier) + .padding(start = 16.dp, top = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically ) { - Column( - modifier = Modifier - .padding(horizontal = DEFAULT_PADDING) - .padding(top = DEFAULT_PADDING) - ) { - Row( - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(MR.strings.one_hand_ui_card_title), style = MaterialTheme.typography.h3) - } - Row( - Modifier.fillMaxWidth().padding(top = 6.dp, bottom = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text(stringResource(MR.strings.one_hand_ui), Modifier.weight(10f), style = MaterialTheme.typography.body1) - - Spacer(Modifier.fillMaxWidth().weight(1f)) - - SharedPreferenceToggle( - appPrefs.oneHandUI, - enabled = true - ) - } - } + Icon( + painterResource(icon), null, Modifier.size(20.dp), + tint = if (isSelected) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + Spacer(Modifier.width(8.dp)) + Text( + text, + color = if (isSelected) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground, + style = MaterialTheme.typography.body1 + ) } } @@ -234,53 +284,120 @@ private fun ChatListCard( } } +private const val BANNER_IMAGE_RATIO = 800f / 505f + @Composable -private fun AddressCreationCard() { - ChatListCard( - close = { - appPrefs.addressCreationCardShown.set(true) - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.simplex_address), - text = generalGetString(MR.strings.address_creation_instruction), +private fun BannerGradientBox(isDark: Boolean, content: @Composable () -> Unit) { + val stops = if (isDark) darkStops else lightStops + val scale = if (isDark) 1.5f else 1.2f + val gp = gradientPoints(1f / BANNER_IMAGE_RATIO, scale) + var size by remember { mutableStateOf(IntSize.Zero) } + val brush = remember(size, isDark) { + if (size.width > 0 && size.height > 0) { + Brush.linearGradient( + colorStops = stops, + start = Offset(gp.startX * size.width, gp.startY * size.height), + end = Offset(gp.endX * size.width, gp.endY * size.height) ) - }, - onCardClick = { - ModalManager.start.showModal { - UserAddressLearnMore(showCreateAddressButton = true) - } + } else { + Brush.linearGradient(colorStops = stops) } - ) { - Box(modifier = Modifier.matchParentSize().padding(end = (DEFAULT_PADDING_HALF + 2.dp) * fontSizeSqrtMultiplier, bottom = 2.dp), contentAlignment = Alignment.BottomEnd) { - TextButton( - onClick = { - ModalManager.start.showModalCloseable { close -> - UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = close) - } - }, - ) { - Text(stringResource(MR.strings.create_address_button), style = MaterialTheme.typography.body1) - } + } + Box( + Modifier.fillMaxWidth().aspectRatio(BANNER_IMAGE_RATIO).background(brush).onSizeChanged { size = it }, + contentAlignment = Alignment.Center + ) { content() } +} + +@Composable +private fun ConnectBannerCard() { + val isDark = isInDarkTheme() + val labelBg = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) + .copy(alpha = appPrefs.inAppBarsAlpha.get()) + val buttonSize = 30.dp * fontSizeSqrtMultiplier + val gap = 3.dp * fontSizeSqrtMultiplier + + Column(horizontalAlignment = Alignment.End) { + IconButton( + onClick = { appPrefs.addressCreationCardShown.set(true) }, + modifier = Modifier.size(buttonSize) + ) { + Icon( + painterResource(MR.images.ic_close), + contentDescription = stringResource(MR.strings.icon_descr_close_button), + modifier = Modifier + .size(buttonSize) + .background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.92f), CircleShape) + .padding(buttonSize * 0.15f), + tint = MaterialTheme.colors.secondary + ) } + Spacer(Modifier.height(gap)) Row( Modifier .fillMaxWidth() - .padding(DEFAULT_PADDING), - verticalAlignment = Alignment.CenterVertically + .height(IntrinsicSize.Min) + .clip(RoundedCornerShape(18.dp)) ) { - Box(Modifier.padding(vertical = 4.dp)) { - Box(Modifier.background(MaterialTheme.colors.primary, CircleShape).padding(12.dp)) { - ProfileImage(size = 37.dp, null, icon = MR.images.ic_mail_filled, color = Color.White, backgroundColor = Color.Red) + Column( + Modifier.weight(1f).clickable { + ModalManager.start.showModalCloseable { close -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) + } + } + ) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isDark) MR.images.banner_create_link_light else MR.images.banner_create_link), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth().aspectRatio(BANNER_IMAGE_RATIO) + ) + } else { + BannerGradientBox(isDark) { + Icon(painterResource(MR.images.ic_add_link), contentDescription = null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colors.primary) + } + } + Box(Modifier.fillMaxWidth().background(labelBg).padding(vertical = 8.dp), contentAlignment = Alignment.Center) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Icon(painterResource(MR.images.ic_add_link), contentDescription = null, modifier = Modifier.size(18.dp), tint = MaterialTheme.colors.primary) + Text(stringResource(MR.strings.new_1_time_link), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onBackground) + } + } else { + Text(stringResource(MR.strings.new_1_time_link), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onBackground) + } } } - Column(modifier = Modifier.padding(start = DEFAULT_PADDING)) { - Text(stringResource(MR.strings.your_simplex_contact_address), style = MaterialTheme.typography.h3) - Spacer(Modifier.fillMaxWidth().padding(DEFAULT_PADDING_HALF)) - Row(verticalAlignment = Alignment.CenterVertically) { - Text(stringResource(MR.strings.how_to_use_simplex_chat), Modifier.padding(end = DEFAULT_SPACE_AFTER_ICON), style = MaterialTheme.typography.body1) - Icon( - painterResource(MR.images.ic_info), - null, + Spacer(Modifier.width(2.dp).fillMaxHeight().background(MaterialTheme.colors.background)) + Column( + Modifier.weight(1f).clickable { + ModalManager.start.showModalCloseable { close -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = close) + } + } + ) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isDark) MR.images.banner_paste_link_light else MR.images.banner_paste_link), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth().aspectRatio(BANNER_IMAGE_RATIO) ) + } else { + BannerGradientBox(isDark) { + Icon(painterResource(MR.images.ic_qr_code_scanner), contentDescription = null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colors.primary) + } + } + Box(Modifier.fillMaxWidth().background(labelBg).padding(vertical = 8.dp), contentAlignment = Alignment.Center) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Icon(painterResource(MR.images.ic_qr_code_scanner), contentDescription = null, modifier = Modifier.size(18.dp), tint = MaterialTheme.colors.primary) + Text(stringResource(if (appPlatform.isAndroid) MR.strings.scan_paste_link else MR.strings.paste_link), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onBackground) + } + } else { + Text(stringResource(if (appPlatform.isAndroid) MR.strings.scan_paste_link else MR.strings.paste_link), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onBackground) + } } } } @@ -289,15 +406,31 @@ private fun AddressCreationCard() { @Composable private fun BoxScope.ChatListWithLoadingScreen(searchText: MutableState, listState: LazyListState) { - if (!chatModel.desktopNoUserNoRemote) { - ChatList(searchText = searchText, listState) + if (chatModel.chatRunning.value == null) { + Text(stringResource(MR.strings.loading_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) + } else if (shouldShowOnboarding()) { + if (appPlatform.isAndroid) AndroidOnboardingCards() + } else { + if (!chatModel.desktopNoUserNoRemote) { + ChatList(searchText = searchText, listState) + } + if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { + Text(stringResource(MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) + } } - if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { - Text( - stringResource( - if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats - ), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary - ) +} + +@Composable +private fun AndroidOnboardingCards() { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val topPad = topPaddingToContent(false) + val bottomPad = if (oneHandUI.value) { + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier + } else { + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + } + Box(Modifier.fillMaxSize().padding(top = topPad, bottom = bottomPad)) { + ConnectOnboardingView() } } @@ -454,31 +587,33 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow } }, title = { - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)) { - Text( - stringResource(MR.strings.your_chats), - color = MaterialTheme.colors.onBackground, - fontWeight = FontWeight.SemiBold, - ) - SubscriptionStatusIndicator( - click = { - ModalManager.start.closeModals() - val summary = serversSummary.value - ModalManager.start.showModalCloseable( - endButtons = { - if (summary != null) { - ShareButton { - val json = Json { - prettyPrint = true + if (!shouldShowOnboarding()) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)) { + Text( + stringResource(MR.strings.your_chats), + color = MaterialTheme.colors.onBackground, + fontWeight = FontWeight.SemiBold, + ) + SubscriptionStatusIndicator( + click = { + ModalManager.start.closeModals() + val summary = serversSummary.value + ModalManager.start.showModalCloseable( + endButtons = { + if (summary != null) { + ShareButton { + val json = Json { + prettyPrint = true + } + val text = json.encodeToString(PresentedServersSummary.serializer(), summary) + clipboard.shareText(text) } - val text = json.encodeToString(PresentedServersSummary.serializer(), summary) - clipboard.shareText(text) } } - } - ) { ServersSummaryView(chatModel.currentRemoteHost.value, serversSummary) } - } - ) + ) { ServersSummaryView(chatModel.currentRemoteHost.value, serversSummary) } + } + ) + } } }, onTitleClick = if (canScrollToZero.value) { { scrollToBottom(scope, listState) } } else null, @@ -827,13 +962,18 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat } } } + if (!oneHandUICardShown.value) { + item { + ToggleChatListCard() + } + } itemsIndexed(chats, key = { _, chat -> chat.remoteHostId to chat.id }) { index, chat -> val nextChatSelected = remember(chat.id, chats) { derivedStateOf { chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value } } ChatListNavLinkView(chat, nextChatSelected) } - if (!oneHandUICardShown.value || !addressCreationCardShown.value) { + if (!addressCreationCardShown.value) { item { ChatListFeatureCards() } @@ -860,14 +1000,6 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat } } - if (!addressCreationCardShown.value) { - LaunchedEffect(chatModel.userAddress.value) { - if (chatModel.userAddress.value != null) { - appPrefs.addressCreationCardShown.set(true) - } - } - } - LaunchedEffect(activeFilter.value) { searchText.value = TextFieldValue("") } @@ -906,19 +1038,11 @@ private fun NoChatsView(searchText: MutableState) { @Composable private fun ChatListFeatureCards() { - val oneHandUI = remember { appPrefs.oneHandUI.state } - val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state } val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state } - Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { - if (!oneHandUICardShown.value && !oneHandUI.value) { - ToggleChatListCard() - } - if (!addressCreationCardShown.value) { - AddressCreationCard() - } - if (!oneHandUICardShown.value && oneHandUI.value) { - ToggleChatListCard() + if (!addressCreationCardShown.value && hasConversations(chatModel.chats.value)) { + Column(modifier = Modifier.padding(16.dp)) { + ConnectBannerCard() } } } @@ -1061,7 +1185,7 @@ private fun ExpandedTagFilterView(tag: PresetTagKind) { is ActiveFilter.PresetTag -> af.tag == tag else -> false } - val (icon, text) = presetTagLabel(tag, active) + val (icon, menuIcon, text) = presetTagLabel(tag, active) val color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary Row( @@ -1081,7 +1205,7 @@ private fun ExpandedTagFilterView(tag: PresetTagKind) { horizontalArrangement = Arrangement.Center ) { Icon( - painterResource(icon), + painterResource(menuIcon ?: icon), stringResource(text), Modifier.size(18.sp.toDp()), tint = color @@ -1123,9 +1247,9 @@ private fun CollapsedTagsFilterView(searchText: MutableState) { contentAlignment = Alignment.Center ) { if (selectedPresetTag != null) { - val (icon, text) = presetTagLabel(selectedPresetTag, true) + val (icon, menuIcon, text) = presetTagLabel(selectedPresetTag, true) Icon( - painterResource(icon), + painterResource(menuIcon ?: icon), stringResource(text), Modifier.size(18.sp.toDp()), tint = MaterialTheme.colors.primary @@ -1171,7 +1295,7 @@ fun ItemPresetFilterAction( showMenu: MutableState, onCloseMenuAction: MutableState<(() -> Unit)> ) { - val (icon, text) = presetTagLabel(presetTag, active) + val (icon, _, text) = presetTagLabel(presetTag, active) ItemAction( stringResource(text), painterResource(icon), @@ -1236,7 +1360,11 @@ fun presetTagMatchesChat(tag: PresetTagKind, chatInfo: ChatInfo, chatStats: Chat else -> false } PresetTagKind.GROUPS -> when (chatInfo) { - is ChatInfo.Group -> chatInfo.groupInfo.businessChat == null + is ChatInfo.Group -> chatInfo.groupInfo.businessChat == null && !chatInfo.groupInfo.isChannel + else -> false + } + PresetTagKind.CHANNELS -> when (chatInfo) { + is ChatInfo.Group -> chatInfo.groupInfo.isChannel else -> false } PresetTagKind.BUSINESS -> when (chatInfo) { @@ -1249,14 +1377,15 @@ fun presetTagMatchesChat(tag: PresetTagKind, chatInfo: ChatInfo, chatStats: Chat } } -private fun presetTagLabel(tag: PresetTagKind, active: Boolean): Pair = +private fun presetTagLabel(tag: PresetTagKind, active: Boolean): Triple = when (tag) { - PresetTagKind.GROUP_REPORTS -> (if (active) MR.images.ic_flag_filled else MR.images.ic_flag) to MR.strings.chat_list_group_reports - PresetTagKind.FAVORITES -> (if (active) MR.images.ic_star_filled else MR.images.ic_star) to MR.strings.chat_list_favorites - PresetTagKind.CONTACTS -> (if (active) MR.images.ic_person_filled else MR.images.ic_person) to MR.strings.chat_list_contacts - PresetTagKind.GROUPS -> (if (active) MR.images.ic_group_filled else MR.images.ic_group) to MR.strings.chat_list_groups - PresetTagKind.BUSINESS -> (if (active) MR.images.ic_work_filled else MR.images.ic_work) to MR.strings.chat_list_businesses - PresetTagKind.NOTES -> (if (active) MR.images.ic_folder_closed_filled else MR.images.ic_folder_closed) to MR.strings.chat_list_notes + PresetTagKind.GROUP_REPORTS -> Triple(if (active) MR.images.ic_flag_filled else MR.images.ic_flag, null, MR.strings.chat_list_group_reports) + PresetTagKind.FAVORITES -> Triple(if (active) MR.images.ic_star_filled else MR.images.ic_star, null, MR.strings.chat_list_favorites) + PresetTagKind.CONTACTS -> Triple(if (active) MR.images.ic_person_filled else MR.images.ic_person, null, MR.strings.chat_list_contacts) + PresetTagKind.GROUPS -> Triple(if (active) MR.images.ic_group_filled else MR.images.ic_group, null, MR.strings.chat_list_groups) + PresetTagKind.CHANNELS -> Triple(if (active) MR.images.ic_bigtop_updates_circle_filled else MR.images.ic_bigtop_updates, MR.images.ic_bigtop_updates, MR.strings.chat_list_channels) + PresetTagKind.BUSINESS -> Triple(if (active) MR.images.ic_work_filled else MR.images.ic_work, null, MR.strings.chat_list_businesses) + PresetTagKind.NOTES -> Triple(if (active) MR.images.ic_folder_closed_filled else MR.images.ic_folder_closed, null, MR.strings.chat_list_notes) } private fun presetCanBeCollapsed(tag: PresetTagKind): Boolean = when (tag) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 9248ac6efe..d749865e10 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -31,6 +31,7 @@ import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.* import chat.simplex.common.views.chat.* +import chat.simplex.common.views.newchat.planAndConnect import chat.simplex.common.views.chat.item.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource @@ -241,18 +242,24 @@ fun ChatPreviewView( Text(previewText.first, color = previewText.second) } else if (ci != null && showChatPreviews) { val (text: CharSequence, inlineTextContent) = when { - ci.meta.itemDeleted == null -> ci.text to null - else -> markedDeletedText(ci, chat.chatInfo) to null + ci.meta.itemDeleted != null -> markedDeletedText(ci, chat.chatInfo) to null + ci.content.msgContent is MsgContent.MCChat -> { + val chatLink = (ci.content.msgContent as MsgContent.MCChat).chatLink + val descr = chatLink.shortDescription?.let { "\n$it" } ?: "" + (chatLink.displayName + descr) to null + } + else -> ci.text(chat.chatInfo.isChannel) to null } - val formattedText = when { - ci.meta.itemDeleted == null -> ci.formattedText - else -> null + val formattedText: List? = when { + ci.meta.itemDeleted != null -> null + ci.content.msgContent is MsgContent.MCChat -> null + else -> ci.formattedText } - val prefix = when (val mc = ci.content.msgContent) { + val prefix = when (ci.content.msgContent) { is MsgContent.MCReport -> buildAnnotatedString { withStyle(SpanStyle(color = Color.Red, fontStyle = FontStyle.Italic)) { - append(if (text.isEmpty()) mc.reason.text else "${mc.reason.text}: ") + append(itemPrefixText(ci)) } } @@ -332,6 +339,19 @@ fun ChatPreviewView( withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } } } + is MsgContent.MCChat -> SmallContentPreview(borderColor = if (mc.chatLink.image != null) MaterialTheme.colors.onSurface.copy(alpha = 0.12f) else Color.Transparent) { + Box( + Modifier.fillMaxSize().clickable { withBGApi { planAndConnect(chat.remoteHostId, mc.chatLink.connLinkStr, linkOwnerSig = mc.ownerSig, close = null) } }, + contentAlignment = Alignment.Center + ) { + val image = mc.chatLink.image + if (image != null) { + Image(base64ToBitmap(image), null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize()) + } else { + Icon(painterResource(mc.chatLink.iconRes), null, Modifier.size(44.sp.toDp()), tint = if (isInDarkTheme()) FileDark else FileLight) + } + } + } else -> {} } } @@ -500,8 +520,8 @@ fun ChatPreviewView( } @Composable -private fun SmallContentPreview(content: @Composable BoxScope.() -> Unit) { - Box(Modifier.padding(top = 2.sp.toDp(), end = 8.sp.toDp()).size(36.sp.toDp()).border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(22)).clip(RoundedCornerShape(22))) { +private fun SmallContentPreview(borderColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f), content: @Composable BoxScope.() -> Unit) { + Box(Modifier.padding(top = 2.sp.toDp(), end = 8.sp.toDp()).size(36.sp.toDp()).border(0.5.dp, borderColor, RoundedCornerShape(22)).clip(RoundedCornerShape(22))) { content() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index aa9847c98a..2be17052ad 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -51,6 +51,9 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) { } } } + is SharedContent.ChatLink -> { + hasSimplexLink = true + } null -> {} } if (chatModel.chats.value.isNotEmpty()) { @@ -98,7 +101,7 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal val navButton: @Composable RowScope.() -> Unit = { when { showSearch -> NavigationButtonBack(hideSearchOnBack) - (users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && remember { chatModel.sharedContent }.value !is SharedContent.Forward -> { + (users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && remember { chatModel.sharedContent }.value !is SharedContent.Forward && remember { chatModel.sharedContent }.value !is SharedContent.ChatLink -> { val allRead = users .filter { u -> !u.user.activeUser && !u.user.hidden } .all { u -> u.unreadCount == 0 } @@ -129,6 +132,8 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal chatModel.sharedContent.value = null if (sharedContent is SharedContent.Forward) { chatModel.chatId.value = sharedContent.fromChatInfo.id + } else if (sharedContent is SharedContent.ChatLink) { + chatModel.chatId.value = sharedContent.groupInfo.id } }) } @@ -144,6 +149,7 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal is SharedContent.Media -> stringResource(MR.strings.share_image) is SharedContent.File -> stringResource(MR.strings.share_file) is SharedContent.Forward -> if (v.chatItems.size > 1) stringResource(MR.strings.forward_multiple) else stringResource(MR.strings.forward_message) + is SharedContent.ChatLink -> stringResource(MR.strings.share_channel) null -> stringResource(MR.strings.share_message) }, color = MaterialTheme.colors.onBackground, @@ -190,7 +196,7 @@ private fun ShareList( val oneHandUI = remember { appPrefs.oneHandUI.state } val chats by remember(search) { derivedStateOf { - val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready }.sortedByDescending { it.chatInfo is ChatInfo.Local } + val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready && it.chatInfo.sendMsgEnabled }.sortedByDescending { it.chatInfo is ChatInfo.Local } filteredChats(mutableStateOf(false), mutableStateOf(null), search, sorted) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index 34d8099951..3d670d1c43 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -273,10 +273,11 @@ class AlertManager { profileFullName: String, profileImage: @Composable () -> Unit, subtitle: String? = null, - confirmText: String = generalGetString(MR.strings.connect_plan_open_chat), - onConfirm: () -> Unit, + information: String? = null, + confirmText: String? = generalGetString(MR.strings.connect_plan_open_chat), + onConfirm: (() -> Unit)? = null, dismissText: String = generalGetString(MR.strings.cancel_verb), - onDismiss: (() -> Unit)?, + onDismiss: (() -> Unit)? = null, ) { showAlert { AlertDialog( @@ -325,7 +326,17 @@ class AlertManager { textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary, - maxLines = 1, + maxLines = 3, + modifier = Modifier.fillMaxWidth() + ) + } + if (information != null) { + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Text( + information, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.body2, + maxLines = 3, modifier = Modifier.fillMaxWidth() ) } @@ -341,16 +352,18 @@ class AlertManager { delay(200) focusRequester.requestFocus() } - TextButton(onClick = { - onConfirm.invoke() - hideAlert() - }, Modifier.focusRequester(focusRequester)) { - Text(confirmText) + if (confirmText != null && onConfirm != null) { + TextButton(onClick = { + onConfirm.invoke() + hideAlert() + }, Modifier.focusRequester(focusRequester)) { + Text(confirmText) + } } TextButton(onClick = { onDismiss?.invoke() hideAlert() - }) { + }, if (confirmText == null) Modifier.focusRequester(focusRequester) else Modifier) { Text(dismissText) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt index afb557cc78..ee63846657 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt @@ -22,7 +22,10 @@ fun AppBarTitle( hostDevice: Pair? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp, - enableAlphaChanges: Boolean = true + enableAlphaChanges: Boolean = true, + overrideTitleColor: Color? = null, + textAlign: TextAlign = TextAlign.Start, + lineHeight: TextUnit = TextUnit.Unspecified ) { val handler = LocalAppBarHandler.current val connection = if (enableAlphaChanges) handler?.connection else null @@ -34,10 +37,12 @@ fun AppBarTitle( } } val theme = CurrentColors.collectAsState() - val titleColor = MaterialTheme.appColors.title - val brush = if (theme.value.base == DefaultTheme.SIMPLEX) + val titleColor = overrideTitleColor ?: MaterialTheme.appColors.title + val brush = if (overrideTitleColor != null) + Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) + else if (theme.value.base == DefaultTheme.SIMPLEX) Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) - else // color is not updated when changing themes if I pass null here + else Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) Column { Text( @@ -48,9 +53,9 @@ fun AppBarTitle( alpha = bottomTitleAlpha(connection) }, overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.h1.copy(brush = brush), + style = MaterialTheme.typography.h1.copy(brush = brush, lineHeight = lineHeight), color = MaterialTheme.colors.primaryVariant, - textAlign = TextAlign.Start + textAlign = textAlign ) if (hostDevice != null) { Box(Modifier.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp).graphicsLayer { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt index 096b6c55ac..c7553b6ed0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt @@ -64,7 +64,7 @@ private fun Modifier.androidBlurredModifier( } } .drawBehind { - drawRect(Color.Black) + drawRect(CurrentColors.value.colors.background) if (onTop) { clipRect { if (backgroundGraphicsLayer.size != IntSize.Zero) { @@ -110,7 +110,7 @@ private fun Modifier.desktopBlurredModifier( clip = blurRadius.value > 0 } .drawBehind { - drawRect(Color.Black) + drawRect(CurrentColors.value.colors.background) if (onTop) { clipRect { if (backgroundGraphicsLayer.size != IntSize.Zero) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt index 30811d5c94..cf3281f776 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt @@ -15,6 +15,7 @@ sealed class SharedContent { data class Media(val text: String, val uris: List): SharedContent() data class File(val text: String, val uri: URI): SharedContent() data class Forward(val chatItems: List, val fromChatInfo: ChatInfo): SharedContent() + data class ChatLink(val groupInfo: GroupInfo): SharedContent() } enum class AnimatedViewState { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt index 9c529e547a..d2a98ae101 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.LinkPreview +import chat.simplex.common.model.NetworkProxyAuth import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.chatViewScrollState @@ -24,67 +25,124 @@ import chat.simplex.common.views.chat.item.CHAT_IMAGE_LAYOUT_ID import chat.simplex.common.views.chat.item.imageViewFullWidth import chat.simplex.res.MR import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.jsoup.Jsoup +import java.net.Authenticator +import java.net.InetSocketAddress +import java.net.PasswordAuthentication +import java.net.Proxy import java.net.URL +import java.util.UUID private const val OG_SELECT_QUERY = "meta[property^=og:]" private const val ICON_SELECT_QUERY = "link[rel^=icon],link[rel^=apple-touch-icon],link[rel^=shortcut icon]" private val IMAGE_SUFFIXES = listOf(".jpg", ".png", ".ico", ".webp", ".gif") +// Authenticator.setDefault is process-global. The mutex serializes preview fetches +// so concurrent calls cannot clobber each other's authenticator, and so the +// snapshot/restore in getLinkPreview is race-free. +private val previewMutex = Mutex() + suspend fun getLinkPreview(url: String): LinkPreview? { return withContext(Dispatchers.IO) { - try { - val title: String? - val u = kotlin.runCatching { URL(url) }.getOrNull() ?: return@withContext null - var imageUri = when { - IMAGE_SUFFIXES.any { u.path.lowercase().endsWith(it) } -> { - title = u.path.substringAfterLast("/") - url - } - else -> { - val connection = Jsoup.connect(url) - .ignoreContentType(true) - .timeout(10000) - .followRedirects(true) - - val response = if (url.lowercase().startsWith("https://x.com/")) { - // Apple sends request with special user-agent which handled differently by X.com. - // Different response that includes video poster from post - connection - .userAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/601.2.4 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.4 facebookexternalhit/1.1 Facebot Twitterbot/1.0") - .execute() - } else { - connection - .execute() - } - val doc = response.parse() - val ogTags = doc.select(OG_SELECT_QUERY) - title = ogTags.firstOrNull { it.attr("property") == "og:title" }?.attr("content") ?: doc.title() - ogTags.firstOrNull { it.attr("property") == "og:image" }?.attr("content") - ?: doc.select(ICON_SELECT_QUERY).firstOrNull { it.attr("rel").contains("icon") }?.attr("href") - } - } - if (imageUri != null) { - imageUri = normalizeImageUri(u, imageUri) + previewMutex.withLock { + try { try { - val stream = URL(imageUri).openStream() - 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) + val title: String? + val u = kotlin.runCatching { URL(url) }.getOrNull() ?: return@withLock null + val useSocksProxy = appPrefs.networkUseSocksProxy.get() + val proxy: Proxy? + if (useSocksProxy) { + val networkProxy = appPrefs.networkProxy.get() + proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(networkProxy.host, networkProxy.port)) + val (authUser, authPass) = when (networkProxy.auth) { + NetworkProxyAuth.USERNAME -> + if (networkProxy.username.isNotEmpty() && networkProxy.password.isNotEmpty()) + networkProxy.username to networkProxy.password + else + null to null + // Per-call random credentials drive Tor-style stream isolation: each + // preview gets its own circuit, and previews don't share a circuit + // with other unauthenticated traffic on the proxy. + NetworkProxyAuth.ISOLATE -> + UUID.randomUUID().toString() to UUID.randomUUID().toString() + } + if (authUser != null && authPass != null) { + Authenticator.setDefault(object : Authenticator() { + override fun getPasswordAuthentication(): PasswordAuthentication? = + // Only respond when the SOCKS proxy itself challenges. A destination + // server returning 401 also triggers RequestorType.SERVER; without + // this gate, the JDK's auto-retry would post our SOCKS credentials + // in an Authorization header to the destination. + if (requestingHost == networkProxy.host && requestingPort == networkProxy.port) + PasswordAuthentication(authUser, authPass.toCharArray()) + else null + }) + } else { + Authenticator.setDefault(null) + } + } else { + proxy = null + Authenticator.setDefault(null) + } + var imageUri = when { + IMAGE_SUFFIXES.any { u.path.lowercase().endsWith(it) } -> { + title = u.path.substringAfterLast("/") + url + } + else -> { + val connection = Jsoup.connect(url) + .ignoreContentType(true) + .timeout(10000) + .followRedirects(true) + .proxy(proxy) + + val response = if (url.lowercase().startsWith("https://x.com/")) { + // Apple sends request with special user-agent which handled differently by X.com. + // Different response that includes video poster from post + connection + .userAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/601.2.4 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.4 facebookexternalhit/1.1 Facebot Twitterbot/1.0") + .execute() + } else { + connection + .execute() + } + val doc = response.parse() + val ogTags = doc.select(OG_SELECT_QUERY) + title = ogTags.firstOrNull { it.attr("property") == "og:title" }?.attr("content") ?: doc.title() + ogTags.firstOrNull { it.attr("property") == "og:image" }?.attr("content") + ?: doc.select(ICON_SELECT_QUERY).firstOrNull { it.attr("rel").contains("icon") }?.attr("href") + } + } + if (imageUri != null) { + imageUri = normalizeImageUri(u, imageUri) + try { + val conn = URL(imageUri).openConnection(proxy ?: Proxy.NO_PROXY) + conn.connectTimeout = 20_000 + conn.readTimeout = 20_000 + val stream = conn.getInputStream() + 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@withLock LinkPreview(url, title, description = "", image) + } + } catch (e: Exception) { + e.printStackTrace() + } } } catch (e: Exception) { e.printStackTrace() } + return@withLock null + } finally { + Authenticator.setDefault(null) } - } catch (e: Exception) { - e.printStackTrace() } - return@withContext null } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index 21520f5424..28c81fbf56 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -111,8 +111,8 @@ class ModalManager(private val placement: ModalPlacement? = null) { fun isLastModalOpen(id: ModalViewId): Boolean = modalViews.lastOrNull()?.id == id - fun showModal(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { - showCustomModal(id = id) { close -> + fun showModal(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, forceAnimated: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { + showCustomModal(id = id, forceAnimated = forceAnimated) { close -> ModalView(close, showClose = showClose, endButtons = endButtons, content = { content() }) } } @@ -123,7 +123,7 @@ class ModalManager(private val placement: ModalPlacement? = null) { } } - fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, id: ModalViewId? = null, modal: @Composable ModalData.(close: () -> Unit) -> Unit) { + fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, id: ModalViewId? = null, forceAnimated: Boolean = false, modal: @Composable ModalData.(close: () -> Unit) -> Unit) { Log.d(TAG, "ModalManager.showCustomModal") val data = ModalData(keyboardCoversBar = keyboardCoversBar) // Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen. @@ -133,7 +133,7 @@ class ModalManager(private val placement: ModalPlacement? = null) { } // 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) + val anim = if (appPlatform.isAndroid) animated else (animated && (modalCount.value > 0 || placement == ModalPlacement.START)) || forceAnimated modalViews.add(ModalViewHolder(id, anim, data, modal)) _modalCount.value = modalViews.size - toRemove.size 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 index c4821d1a20..424d500085 100644 --- 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 @@ -537,6 +537,20 @@ fun UriHandler.openUriCatching(uri: String) { } } +fun UriHandler.openExternalLink(uri: String) { + val uriHandler = this + if (uri.startsWith("https://simplex.chat/contact#") || (uri.startsWith("https://smp") && ".simplex.im/a#" in uri)) { + uriHandler.openVerifiedSimplexUri(uri) + } else { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.open_external_link_title), + text = uri, + confirmText = generalGetString(MR.strings.open_verb), + onConfirm = { uriHandler.openUriCatching(uri) } + ) + } +} + fun IntSize.Companion.Saver(): Saver = Saver( save = { it.width to it.height }, restore = { IntSize(it.first, it.second) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt index cf10f5e545..09372636ab 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt @@ -23,6 +23,8 @@ import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.getUserServers import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import androidx.compose.ui.layout.ContentScale +import chat.simplex.common.BuildConfigCommon import chat.simplex.common.views.* import chat.simplex.common.views.chat.group.GroupLinkView import chat.simplex.common.views.chatlist.openGroupChat @@ -65,7 +67,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit withBGApi { openGroupChat(null, gInfo.groupId) ModalManager.end.showModalCloseable(true) { close -> - GroupLinkView(chatModel, rhId = null, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = null, creatingGroup = true, isChannel = true, close = close) + GroupLinkView(chatModel, rhId = null, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = null, creatingGroup = true, isChannel = true, shareGroupInfo = gInfo, close = close) } } } @@ -110,7 +112,10 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit fullName = "", shortDescr = null, image = profileImage.value, - groupPreferences = GroupPreferences(history = GroupPreference(GroupFeatureEnabled.ON)) + groupPreferences = GroupPreferences( + history = GroupPreference(GroupFeatureEnabled.ON), + support = GroupPreference(GroupFeatureEnabled.OFF) + ) ) creationInProgress.value = true withBGApi { @@ -130,19 +135,31 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit relayIds = relayIds, groupProfile = profile ) - if (result != null) { - val (gI, gL, gR) = result - withContext(Dispatchers.Main) { - chatModel.chatsContext.updateGroup(rhId = null, gI) - chatModel.creatingChannelId.value = gI.id - groupInfo.value = gI - groupLink.value = gL - groupRelays.value = gR.sortedBy { relayDisplayName(it) } - ChannelRelaysModel.set(gI.groupId, gR) - creationInProgress.value = false + when (result) { + is ChatController.PublicGroupCreationResult.Created -> { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId = null, result.groupInfo) + chatModel.creatingChannelId.value = result.groupInfo.id + groupInfo.value = result.groupInfo + groupLink.value = result.groupLink + groupRelays.value = result.groupRelays.sortedBy { relayDisplayName(it) } + ChannelRelaysModel.set(result.groupInfo.groupId, result.groupRelays) + creationInProgress.value = false + } + } + is ChatController.PublicGroupCreationResult.CreationFailed -> { + withContext(Dispatchers.Main) { + creationInProgress.value = false + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.error_creating_channel), + text = generalGetString(MR.strings.relay_results) + "\n" + + result.addRelayResults.joinToString("\n") { "${chatRelayDisplayName(it.relay)}: ${it.relayError?.let { e -> ChatController.connErrorText(e) } ?: "ok"}" } + ) + } + } + null -> { + withContext(Dispatchers.Main) { creationInProgress.value = false } } - } else { - withContext(Dispatchers.Main) { creationInProgress.value = false } } } catch (e: Exception) { withContext(Dispatchers.Main) { @@ -168,6 +185,7 @@ private suspend fun chooseRandomRelays(): List { val operatorGroups = mutableListOf>() var customRelays = mutableListOf() for (op in servers) { + if (op.operator?.enabled == false) continue val relays = op.chatRelays.filter { it.enabled && !it.deleted && it.chatRelayId != null } if (relays.isEmpty()) continue if (op.operator != null) { @@ -200,6 +218,7 @@ private suspend fun chooseRandomRelays(): List { private suspend fun checkHasRelays(): Boolean { val servers = try { getUserServers(rh = null) } catch (_: Exception) { null } ?: return false return servers.any { op -> + (op.operator?.enabled ?: true) && op.chatRelays.any { it.enabled && !it.deleted && it.chatRelayId != null } } } @@ -240,22 +259,37 @@ private fun ProfileStepView( ) { ModalView(close = close) { ColumnWithScrollBar { - AppBarTitle(generalGetString(MR.strings.create_channel_title)) - Box( + AppBarTitle(generalGetString(MR.strings.create_channel_title), bottomPadding = DEFAULT_PADDING_HALF) + Row( Modifier .fillMaxWidth() - .padding(bottom = 24.dp), - contentAlignment = Alignment.Center + .padding(vertical = DEFAULT_PADDING_HALF), + horizontalArrangement = if (BuildConfigCommon.SIMPLEX_ASSETS) Arrangement.SpaceEvenly else Arrangement.Center, + verticalAlignment = Alignment.CenterVertically ) { - Box(contentAlignment = Alignment.TopEnd) { - Box(contentAlignment = Alignment.Center) { - ProfileImage(108.dp, image = profileImage.value) - EditImageButton { scope.launch { bottomSheetModalState.show() } } - } - if (profileImage.value != null) { - DeleteImageButton { profileImage.value = null } + // Padding offsets transparent space built into 3D asset + Box( + modifier = if (BuildConfigCommon.SIMPLEX_ASSETS) Modifier.padding(horizontal = 3.dp) else Modifier, + contentAlignment = Alignment.Center + ) { + Box(contentAlignment = Alignment.TopEnd) { + Box(contentAlignment = Alignment.Center) { + ProfileImage(128.dp, image = profileImage.value, icon = MR.images.ic_bigtop_updates_circle_filled) + EditImageButton { scope.launch { bottomSheetModalState.show() } } + } + if (profileImage.value != null) { + DeleteImageButton { profileImage.value = null } + } } } + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isInDarkTheme()) MR.images.create_channel_light else MR.images.create_channel), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.height(140.dp) + ) + } } Row( Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), @@ -330,16 +364,23 @@ private fun ProgressStepView( val activeCount = groupRelays.value.count { it.relayStatus == RelayStatus.RsActive && relayMemberConnFailed(chatModel, it) == null } val total = groupRelays.value.size + fun showCancelAlert() { + val active = groupRelays.value.count { it.relayStatus == RelayStatus.RsActive && relayMemberConnFailed(chatModel, it) == null } + val tot = groupRelays.value.size + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.cancel_creating_channel_question), + text = String.format(generalGetString(MR.strings.cancel_channel_alert_msg), gInfo.groupProfile.displayName, active, tot), + confirmText = generalGetString(MR.strings.cancel_verb), + onConfirm = cancelChannelCreation, + dismissText = generalGetString(MR.strings.wait_verb), + destructive = true, + ) + } + if (appPlatform.isDesktop) { DisposableEffect(Unit) { chatModel.centerPanelBackgroundClickHandler = { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.cancel_creating_channel_question), - confirmText = generalGetString(MR.strings.cancel_creating_channel_confirm), - onConfirm = cancelChannelCreation, - dismissText = generalGetString(MR.strings.wait_verb), - destructive = true, - ) + showCancelAlert() true } onDispose { @@ -361,13 +402,8 @@ private fun ProgressStepView( } ModalView( - close = cancelChannelCreation, + close = { showCancelAlert() }, showClose = false, - endButtons = { - TextButton(onClick = cancelChannelCreation) { - Text(generalGetString(MR.strings.cancel_verb)) - } - } ) { ColumnWithScrollBar { AppBarTitle(generalGetString(MR.strings.creating_channel)) @@ -376,7 +412,7 @@ private fun ProgressStepView( Modifier.fillMaxWidth().padding(bottom = 8.dp), contentAlignment = Alignment.Center ) { - ProfileImage(108.dp, image = gInfo.groupProfile.image) + ProfileImage(108.dp, image = gInfo.groupProfile.image, icon = MR.images.ic_bigtop_updates_circle_filled) } Text( gInfo.groupProfile.displayName, @@ -440,10 +476,17 @@ private fun ProgressStepView( Spacer(Modifier.height(16.dp)) SectionView { + SettingsActionItem( + painterResource(MR.images.ic_delete), + generalGetString(MR.strings.button_cancel_and_delete_channel), + click = { showCancelAlert() }, + textColor = Color.Red, + iconColor = Color.Red, + ) val enabled = activeCount > 0 SettingsActionItem( - painterResource(MR.images.ic_link), - generalGetString(MR.strings.channel_link), + painterResource(MR.images.ic_check), + generalGetString(MR.strings.continue_to_next_step), click = { if (activeCount >= total) { onLinkReady() @@ -465,7 +508,7 @@ private fun ProgressStepView( AlertManager.shared.hideAlert() onLinkReady() }) { - Text(generalGetString(MR.strings.proceed_verb)) + Text(generalGetString(MR.strings.continue_to_next_step)) } } } @@ -474,7 +517,7 @@ private fun ProgressStepView( AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.not_all_relays_connected), text = alertText, - confirmText = generalGetString(MR.strings.proceed_verb), + confirmText = generalGetString(MR.strings.continue_to_next_step), onConfirm = { onLinkReady() } ) } @@ -545,11 +588,16 @@ fun relayDisplayName(relay: GroupRelay): String { return "relay ${relay.groupRelayId}" } +fun chatRelayDisplayName(relay: UserChatRelay): String { + if (relay.displayName.isNotEmpty()) return relay.displayName + return relay.address +} @Composable -fun RelayStatusIndicator(status: RelayStatus, connFailed: Boolean = false) { - val color = if (connFailed) Color.Red else if (status == RelayStatus.RsActive) Color.Green else WarningYellow - val text = if (connFailed) generalGetString(MR.strings.relay_status_failed) else status.text +fun RelayStatusIndicator(status: RelayStatus, connFailed: Boolean = false, memberStatus: GroupMemberStatus? = null) { + val removed = memberStatus in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) + val color = if (connFailed || removed) Color.Red else if (status == RelayStatus.RsActive) Color.Green else WarningYellow + val text = if (connFailed) generalGetString(MR.strings.relay_status_failed) else if (memberStatus == GroupMemberStatus.MemLeft) generalGetString(MR.strings.relay_conn_status_removed_by_operator) else if (removed) generalGetString(MR.strings.relay_conn_status_removed) else status.text Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index 0494cbb463..a54d2e42e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -27,6 +27,8 @@ import chat.simplex.common.views.* import chat.simplex.common.views.chat.group.GroupLinkView import chat.simplex.common.views.chatlist.openGroupChat import chat.simplex.common.views.usersettings.* +import androidx.compose.ui.layout.ContentScale +import chat.simplex.common.BuildConfigCommon import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -99,22 +101,37 @@ fun AddGroupLayout( ) { ModalView(close = close) { ColumnWithScrollBar { - AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId)) - Box( + AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId), bottomPadding = DEFAULT_PADDING_HALF) + Row( Modifier .fillMaxWidth() - .padding(bottom = 24.dp), - contentAlignment = Alignment.Center + .padding(vertical = DEFAULT_PADDING_HALF), + horizontalArrangement = if (BuildConfigCommon.SIMPLEX_ASSETS) Arrangement.SpaceEvenly else Arrangement.Center, + verticalAlignment = Alignment.CenterVertically ) { - Box(contentAlignment = Alignment.TopEnd) { - Box(contentAlignment = Alignment.Center) { - ProfileImage(108.dp, image = profileImage.value) - EditImageButton { scope.launch { bottomSheetModalState.show() } } - } - if (profileImage.value != null) { - DeleteImageButton { profileImage.value = null } + // Padding offsets transparent space built into 3D asset + Box( + modifier = if (BuildConfigCommon.SIMPLEX_ASSETS) Modifier.padding(horizontal = 3.dp) else Modifier, + contentAlignment = Alignment.Center + ) { + Box(contentAlignment = Alignment.TopEnd) { + Box(contentAlignment = Alignment.Center) { + ProfileImage(128.dp, image = profileImage.value, icon = MR.images.ic_supervised_user_circle_filled) + EditImageButton { scope.launch { bottomSheetModalState.show() } } + } + if (profileImage.value != null) { + DeleteImageButton { profileImage.value = null } + } } } + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isInDarkTheme()) MR.images.create_group_light else MR.images.create_group), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.height(140.dp) + ) + } } Row(Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index 68c7d5b3f1..cafad97574 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -24,6 +24,7 @@ enum class ConnectionLinkType { suspend fun planAndConnect( rhId: Long?, shortOrFullLink: String, + linkOwnerSig: LinkOwnerSig? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? = null, filterKnownContact: ((Contact) -> Unit)? = null, @@ -44,12 +45,13 @@ suspend fun planAndConnect( inProgress.value = false cleanup?.invoke() } - return planAndConnectTask(rhId, shortOrFullLink, close, cleanup, filterKnownContact, filterKnownGroup, inProgress) + return planAndConnectTask(rhId, shortOrFullLink, linkOwnerSig, close, cleanup, filterKnownContact, filterKnownGroup, inProgress) } private suspend fun planAndConnectTask( rhId: Long?, shortOrFullLink: String, + linkOwnerSig: LinkOwnerSig? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? = null, filterKnownContact: ((Contact) -> Unit)? = null, @@ -66,7 +68,7 @@ private suspend fun planAndConnectTask( cleanup?.invoke() completable.complete(!completable.isActive) } - val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink, inProgress = inProgress) + val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink, linkOwnerSig, inProgress = inProgress) connectProgressManager.stopConnectProgress() if (!inProgress.value) { return completable } if (result != null) { @@ -85,6 +87,7 @@ private suspend fun planAndConnectTask( rhId, connectionLink, connectionPlan.invitationLinkPlan.contactSLinkData_, + ownerVerification = connectionPlan.invitationLinkPlan.ownerVerification, close, cleanup ) @@ -96,6 +99,7 @@ private suspend fun planAndConnectTask( text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText, connectDestructive = false, cleanup = cleanup, + ownerVerification = connectionPlan.invitationLinkPlan.ownerVerification, ) } InvitationLinkPlan.OwnLink -> { @@ -146,6 +150,7 @@ private suspend fun planAndConnectTask( rhId, connectionLink, connectionPlan.contactAddressPlan.contactSLinkData_, + ownerVerification = connectionPlan.contactAddressPlan.ownerVerification, close, cleanup ) @@ -157,6 +162,7 @@ private suspend fun planAndConnectTask( text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText, connectDestructive = false, cleanup, + ownerVerification = connectionPlan.contactAddressPlan.ownerVerification, ) } ContactAddressPlan.OwnLink -> { @@ -215,6 +221,7 @@ private suspend fun planAndConnectTask( connectionLink, connectionPlan.groupLinkPlan.groupSLinkInfo_, connectionPlan.groupLinkPlan.groupSLinkData_, + ownerVerification = connectionPlan.groupLinkPlan.ownerVerification, close, cleanup ) @@ -226,6 +233,7 @@ private suspend fun planAndConnectTask( text = generalGetString(MR.strings.you_will_join_group) + linkText, connectDestructive = false, cleanup = cleanup, + ownerVerification = connectionPlan.groupLinkPlan.ownerVerification, ) } is GroupLinkPlan.OwnLink -> { @@ -281,6 +289,33 @@ private suspend fun planAndConnectTask( cleanup() } } + is GroupLinkPlan.NoRelays -> { + Log.d(TAG, "planAndConnect, .GroupLink, .NoRelays") + val groupSLinkData = connectionPlan.groupLinkPlan.groupSLinkData_ + if (groupSLinkData != null) { + AlertManager.privacySensitive.showOpenChatAlert( + profileName = groupSLinkData.groupProfile.displayName, + profileFullName = groupSLinkData.groupProfile.fullName, + profileImage = { + ProfileImage( + size = alertProfileImageSize, + image = groupSLinkData.groupProfile.image, + icon = MR.images.ic_bigtop_updates_circle_filled + ) + }, + subtitle = generalGetString(MR.strings.channel_no_active_relays_try_later), + confirmText = null, + dismissText = generalGetString(MR.strings.ok), + onDismiss = { cleanup() } + ) + } else { + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.channel_temporarily_unavailable), + generalGetString(MR.strings.channel_no_active_relays_try_later) + ) + cleanup() + } + } } is ConnectionPlan.Error -> { Log.d(TAG, "planAndConnect, error ${connectionPlan.chatError}") @@ -348,10 +383,12 @@ fun askCurrentOrIncognitoProfileAlert( text: String? = null, connectDestructive: Boolean, cleanup: (() -> Unit)?, + ownerVerification: OwnerVerification? = null, ) { + val fullText = listOfNotNull(text, ownerVerificationMessage(ownerVerification)).joinToString("\n\n").ifEmpty { null } AlertManager.privacySensitive.showAlertDialogButtonsColumn( title = title, - text = text, + text = fullText, buttons = { Column { val connectColor = if (connectDestructive) MaterialTheme.colors.error else MaterialTheme.colors.primary @@ -546,6 +583,7 @@ fun showPrepareContactAlert( rhId: Long?, connectionLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, + ownerVerification: OwnerVerification? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? ) { @@ -562,9 +600,11 @@ fun showPrepareContactAlert( else MR.images.ic_account_circle_filled ) }, + information = ownerVerificationMessage(ownerVerification), confirmText = generalGetString(MR.strings.connect_plan_open_new_chat), onConfirm = { AlertManager.privacySensitive.hideAlert() + ModalManager.closeAllModalsEverywhere() withBGApi { val chat = chatModel.controller.apiPrepareContact(rhId, connectionLink, contactShortLinkData) if (chat != null) { @@ -587,6 +627,7 @@ fun showPrepareGroupAlert( connectionLink: CreatedConnLink, groupShortLinkInfo: GroupShortLinkInfo?, groupShortLinkData: GroupShortLinkData, + ownerVerification: OwnerVerification? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? ) { @@ -599,10 +640,11 @@ fun showPrepareGroupAlert( ProfileImage( size = alertProfileImageSize, image = groupShortLinkData.groupProfile.image, - icon = if (isChannel) MR.images.ic_bigtop_updates_padded else MR.images.ic_supervised_user_circle_filled + icon = if (isChannel) MR.images.ic_bigtop_updates_circle_filled else MR.images.ic_supervised_user_circle_filled ) }, subtitle = subscriberCount, + information = ownerVerificationMessage(ownerVerification), confirmText = generalGetString(if (isChannel) MR.strings.connect_plan_open_new_channel else MR.strings.connect_plan_open_new_group), onConfirm = { AlertManager.privacySensitive.hideAlert() @@ -630,3 +672,9 @@ fun showPrepareGroupAlert( } ) } + +fun ownerVerificationMessage(ov: OwnerVerification?): String? = when (ov) { + is OwnerVerification.Verified -> generalGetString(MR.strings.owner_verification_passed) + is OwnerVerification.Failed -> String.format(generalGetString(MR.strings.owner_verification_failed), ov.reason) + null -> null +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 292aa10f70..1eceaf4158 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -74,7 +74,7 @@ fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) { Column(Modifier.align(Alignment.BottomCenter)) { DefaultAppBar( navigationButton = { NavigationButtonBack(onButtonClicked = close) }, - fixedTitleText = generalGetString(MR.strings.new_message), + fixedTitleText = generalGetString(MR.strings.new_chat), onTop = false, ) } @@ -359,7 +359,7 @@ private fun ModalData.NewChatSheetLayout( item { Box(Modifier.padding(top = blankSpaceSize)) { AppBarTitle( - stringResource(MR.strings.new_message), + stringResource(MR.strings.new_chat), hostDevice(rh?.remoteHostId), bottomPadding = DEFAULT_PADDING ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index f520a86999..72311cd7fe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -21,9 +21,11 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp @@ -39,6 +41,7 @@ import chat.simplex.common.views.chat.item.CIFileViewScope import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* +import chat.simplex.common.BuildConfigCommon import chat.simplex.res.MR import kotlinx.coroutines.* @@ -47,7 +50,7 @@ enum class NewChatOption { } @Composable -fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRCodeScanner: Boolean = false, close: () -> Unit) { +fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRCodeScanner: Boolean = false, onboarding: Boolean = false, close: () -> Unit) { val selection = remember { stateGetOrPut("selection") { selection } } val showQRCodeScanner = remember { stateGetOrPut("showQRCodeScanner") { showQRCodeScanner } } val contactConnection: MutableState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(chatModel.showingInvitation.value?.conn) } @@ -104,60 +107,71 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC } } - BoxWithConstraints { + if (onboarding) { ColumnWithScrollBar { - AppBarTitle(stringResource(MR.strings.new_chat), hostDevice(rh?.remoteHostId), bottomPadding = DEFAULT_PADDING) - val scope = rememberCoroutineScope() - val pagerState = rememberPagerState( - initialPage = selection.value.ordinal, - initialPageOffsetFraction = 0f - ) { NewChatOption.values().size } - KeyChangeEffect(pagerState.currentPage) { - selection.value = NewChatOption.values()[pagerState.currentPage] + Spacer(Modifier.height(DEFAULT_PADDING)) + when (selection.value) { + NewChatOption.INVITE -> PrepareAndInviteView(rh?.remoteHostId, contactConnection, connLinkInvitation, creatingConnReq, onboarding = true) + NewChatOption.CONNECT -> ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close, onboarding = true) } - TabRow( - selectedTabIndex = pagerState.currentPage, - backgroundColor = Color.Transparent, - contentColor = MaterialTheme.colors.primary, - ) { - tabTitles.forEachIndexed { index, it -> - LeadingIconTab( - selected = pagerState.currentPage == index, - onClick = { - scope.launch { - pagerState.animateScrollToPage(index) - } - }, - text = { Text(it, fontSize = 13.sp) }, - icon = { - Icon( - if (NewChatOption.INVITE.ordinal == index) painterResource(MR.images.ic_repeat_one) else painterResource(MR.images.ic_qr_code), - it - ) - }, - selectedContentColor = MaterialTheme.colors.primary, - unselectedContentColor = MaterialTheme.colors.secondary, - ) + SectionBottomSpacer() + } + } else { + BoxWithConstraints { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.new_chat), hostDevice(rh?.remoteHostId), bottomPadding = DEFAULT_PADDING) + val scope = rememberCoroutineScope() + val pagerState = rememberPagerState( + initialPage = selection.value.ordinal, + initialPageOffsetFraction = 0f + ) { NewChatOption.values().size } + KeyChangeEffect(pagerState.currentPage) { + selection.value = NewChatOption.values()[pagerState.currentPage] } - } - - HorizontalPager(state = pagerState, Modifier, pageNestedScrollConnection = LocalAppBarHandler.current!!.connection, verticalAlignment = Alignment.Top, userScrollEnabled = appPlatform.isAndroid) { index -> - Column( - Modifier - .fillMaxWidth() - .heightIn(min = this@BoxWithConstraints.maxHeight - 150.dp), - verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connLinkInvitation.connFullLink.isEmpty()) Arrangement.Center else Arrangement.Top + TabRow( + selectedTabIndex = pagerState.currentPage, + backgroundColor = Color.Transparent, + contentColor = MaterialTheme.colors.primary, ) { - Spacer(Modifier.height(DEFAULT_PADDING)) - when (index) { - NewChatOption.INVITE.ordinal -> { - PrepareAndInviteView(rh?.remoteHostId, contactConnection, connLinkInvitation, creatingConnReq) - } - NewChatOption.CONNECT.ordinal -> { - ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close) - } + tabTitles.forEachIndexed { index, it -> + LeadingIconTab( + selected = pagerState.currentPage == index, + onClick = { + scope.launch { + pagerState.animateScrollToPage(index) + } + }, + text = { Text(it, fontSize = 13.sp) }, + icon = { + Icon( + if (NewChatOption.INVITE.ordinal == index) painterResource(MR.images.ic_repeat_one) else painterResource(MR.images.ic_qr_code), + it + ) + }, + selectedContentColor = MaterialTheme.colors.primary, + unselectedContentColor = MaterialTheme.colors.secondary, + ) + } + } + + HorizontalPager(state = pagerState, Modifier, pageNestedScrollConnection = LocalAppBarHandler.current!!.connection, verticalAlignment = Alignment.Top, userScrollEnabled = appPlatform.isAndroid) { index -> + Column( + Modifier + .fillMaxWidth() + .heightIn(min = this@BoxWithConstraints.maxHeight - 150.dp), + verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connLinkInvitation.connFullLink.isEmpty()) Arrangement.Center else Arrangement.Top + ) { + Spacer(Modifier.height(DEFAULT_PADDING)) + when (index) { + NewChatOption.INVITE.ordinal -> { + PrepareAndInviteView(rh?.remoteHostId, contactConnection, connLinkInvitation, creatingConnReq) + } + NewChatOption.CONNECT.ordinal -> { + ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close) + } + } + SectionBottomSpacer() } - SectionBottomSpacer() } } } @@ -165,12 +179,13 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC } @Composable -private fun PrepareAndInviteView(rhId: Long?, contactConnection: MutableState, connLinkInvitation: CreatedConnLink, creatingConnReq: MutableState) { +private fun PrepareAndInviteView(rhId: Long?, contactConnection: MutableState, connLinkInvitation: CreatedConnLink, creatingConnReq: MutableState, onboarding: Boolean = false) { if (connLinkInvitation.connFullLink.isNotEmpty()) { InviteView( rhId, connLinkInvitation = connLinkInvitation, contactConnection = contactConnection, + onboarding = onboarding, ) } else if (creatingConnReq.value) { CreatingLinkProgressView() @@ -448,23 +463,53 @@ fun ActiveProfilePicker( } @Composable -private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contactConnection: MutableState) { +private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contactConnection: MutableState, onboarding: Boolean = false) { val showShortLink = remember { mutableStateOf(true) } - Spacer(Modifier.height(10.dp)) - SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isInDarkTheme()) { + if (onboarding) MR.images.one_time_link_light else MR.images.one_time_link_small_light + } else { + if (onboarding) MR.images.one_time_link else MR.images.one_time_link_small + }), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxWidth() + ) + } else { + Spacer(Modifier.height(10.dp)) + } + + if (onboarding) { + Text( + stringResource(MR.strings.onboarding_send_1_time_link), + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), + style = MaterialTheme.typography.body1 + ) LinkTextView(connLinkInvitation.simplexChatUri(short = showShortLink.value), true) - } - - Spacer(Modifier.height(DEFAULT_PADDING)) - - SectionViewWithButton( - stringResource(MR.strings.or_show_this_qr_code).uppercase(), - titleButton = if (connLinkInvitation.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null - ) { + Text( + stringResource(MR.strings.onboarding_or_show_qr_code), + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), + style = MaterialTheme.typography.body1 + ) SimpleXCreatedLinkQRCode(connLinkInvitation, short = showShortLink.value, onShare = { chatModel.markShowingInvitationUsed() }) + } else { + SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) { + LinkTextView(connLinkInvitation.simplexChatUri(short = showShortLink.value), true) + } + + Spacer(Modifier.height(DEFAULT_PADDING)) + + SectionViewWithButton( + stringResource(MR.strings.or_show_this_qr_code).uppercase(), + titleButton = if (connLinkInvitation.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null + ) { + SimpleXCreatedLinkQRCode(connLinkInvitation, short = showShortLink.value, onShare = { chatModel.markShowingInvitationUsed() }) + } } + if (!onboarding) { Spacer(Modifier.height(DEFAULT_PADDING)) val incognito by remember(chatModel.showingInvitation.value?.conn?.incognito, controller.appPrefs.incognito.get()) { derivedStateOf { @@ -531,6 +576,7 @@ private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contact SectionTextFooter(generalGetString(MR.strings.connect__a_new_random_profile_will_be_shared)) } } + } } @Composable @@ -577,13 +623,26 @@ fun AddContactLearnMoreButton() { } @Composable -private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState, pastedLink: MutableState, close: () -> Unit) { +private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState, pastedLink: MutableState, close: () -> Unit, onboarding: Boolean = false) { DisposableEffect(Unit) { onDispose { connectProgressManager.cancelConnectProgress() } } + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isInDarkTheme()) { + if (onboarding) MR.images.connect_via_link_light else MR.images.connect_via_link_small_light + } else { + if (onboarding) MR.images.connect_via_link else MR.images.connect_via_link_small + }), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxWidth() + ) + } + SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase(), headerBottomPadding = 5.dp) { PasteLinkView(rhId, pastedLink, showQRCodeScanner, close) } @@ -625,7 +684,7 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState, showQRC } }) { Box(Modifier.weight(1f)) { - Text(stringResource(MR.strings.tap_to_paste_link)) + Text(stringResource(MR.strings.tap_to_paste_link), color = MaterialTheme.colors.primary) } if (connectProgressManager.showConnectProgress != null) { CIFileViewScope.progressIndicator(sizeMultiplier = 0.6f) @@ -681,6 +740,13 @@ fun LinkTextView(link: String, share: Boolean) { // So using BasicTextField + manual ... Text("…", fontSize = 16.sp) if (share) { + Spacer(Modifier.width(DEFAULT_PADDING)) + IconButton({ + chatModel.markShowingInvitationUsed() + clipboard.setText(AnnotatedString(link)) + }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_content_copy), null, tint = MaterialTheme.colors.primary) + } Spacer(Modifier.width(DEFAULT_PADDING)) IconButton({ chatModel.markShowingInvitationUsed() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/OnboardingCards.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/OnboardingCards.kt new file mode 100644 index 0000000000..26007c74af --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/OnboardingCards.kt @@ -0,0 +1,425 @@ +package chat.simplex.common.views.newchat + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.colorspace.ColorSpaces +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layout +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.BuildConfigCommon +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.UserAddressView +import chat.simplex.res.MR +import kotlinx.coroutines.launch +import kotlin.math.cos +import kotlin.math.sin + +private const val CARD_HEIGHT_RATIO = 0.75f +private const val GRADIENT_ANGLE_RAD = 80.0 * Math.PI / 180.0 + +@Composable +fun shouldShowOnboarding(): Boolean { + val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state } + val chats = chatModel.chats.value + return !addressCreationCardShown.value && chats.isNotEmpty() && !hasConversations(chats) +} + +fun hasConversations(chats: List): Boolean = + chats.any { chat -> + when (val c = chat.chatInfo) { + is ChatInfo.Local -> false + is ChatInfo.Direct -> !c.contact.chatDeleted && !c.contact.isContactCard + is ChatInfo.Group -> true + is ChatInfo.ContactRequest -> false + is ChatInfo.ContactConnection -> false + is ChatInfo.InvalidJSON -> false + } + } + +internal data class GradientEndpoints(val startX: Float, val startY: Float, val endX: Float, val endY: Float) + +internal fun gradientPoints(aspectRatio: Float, scale: Float): GradientEndpoints { + val r = aspectRatio.toDouble() + val s = scale.toDouble() + val dx = cos(GRADIENT_ANGLE_RAD) + val dy = -sin(GRADIENT_ANGLE_RAD) / r + val dLenSq = dx * dx + dy * dy + val projections = doubleArrayOf( + -0.5 * dx + (-0.5) * dy, + 0.5 * dx + (-0.5) * dy, + -0.5 * dx + 0.5 * dy, + 0.5 * dx + 0.5 * dy + ) + val tMin = projections.min() + val tMax = projections.max() + val startX = 0.5 + tMin * dx / dLenSq + val startY = 0.5 + tMin * dy / dLenSq + val endX = 0.5 + tMax * dx / dLenSq + val endY = 0.5 + tMax * dy / dLenSq + return GradientEndpoints( + startX = (0.5 + (startX - 0.5) * s).toFloat(), + startY = (0.5 + (startY - 0.5) * s).toFloat(), + endX = (0.5 + (endX - 0.5) * s).toFloat(), + endY = (0.5 + (endY - 0.5) * s).toFloat() + ) +} + +internal val lightStops = arrayOf( + 0.0f to oklch(0.9219f, 0.0431f, 249.4f), + 0.5f to oklch(0.9198f, 0.0471f, 240.7f), + 0.9f to oklch(0.9772f, 0.0358f, 196.6f), + 0.95f to oklch(0.9829f, 0.0104f, 70.0f), + 1.0f to oklch(0.9886f, 0.0272f, 99.1f) +) + +internal val darkStops = arrayOf( + 0.4f to oklch(0.1578f, 0.0609f, 267.3f), + 0.72f to oklch(0.4729f, 0.1574f, 267.3f), + 0.9f to oklch(0.9024f, 0.0760f, 202.8f), + 0.95f to oklch(0.9384f, 0.0354f, 65.0f), + 1.0f to oklch(0.9744f, 0.0370f, 88.4f) +) + +private fun Modifier.maxHeightByWidthRatio(ratio: Float) = layout { measurable, constraints -> + val maxH = (constraints.maxWidth * ratio).toInt().coerceAtMost(constraints.maxHeight) + val placeable = measurable.measure(constraints.copy(minHeight = 0, maxHeight = maxH)) + layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } +} + +@Composable +fun OnboardingCardView( + imageName: dev.icerock.moko.resources.ImageResource, + imageNameLight: dev.icerock.moko.resources.ImageResource, + icon: dev.icerock.moko.resources.ImageResource, + title: String, + subtitle: String? = null, + labelHeightRatio: Float, + onClick: () -> Unit +) { + var imageAreaSize by remember { mutableStateOf(IntSize.Zero) } + val isDark = isInDarkTheme() + val stops = if (isDark) darkStops else lightStops + val scale = if (isDark) 1.5f else 1.2f + + val brush = remember(imageAreaSize, isDark) { + if (imageAreaSize.width > 0 && imageAreaSize.height > 0) { + val aspect = imageAreaSize.height.toFloat() / imageAreaSize.width.toFloat() + val gp = gradientPoints(aspect, scale) + Brush.linearGradient( + colorStops = stops, + start = Offset(gp.startX * imageAreaSize.width, gp.startY * imageAreaSize.height), + end = Offset(gp.endX * imageAreaSize.width, gp.endY * imageAreaSize.height) + ) + } else { + Brush.linearGradient(colorStops = stops) + } + } + + val labelBg = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) + .copy(alpha = appPrefs.inAppBarsAlpha.get()) + + Box( + Modifier + .fillMaxSize() + .clip(RoundedCornerShape(24.dp)) + .clickable(onClick = onClick) + ) { + Column(Modifier.fillMaxSize()) { + Box( + Modifier + .fillMaxWidth() + .weight(1f) + .background(brush) + .onSizeChanged { imageAreaSize = it } + ) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isDark) imageNameLight else imageName), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxSize() + ) + } else { + Icon( + painterResource(icon), + contentDescription = null, + modifier = Modifier.size(64.dp).align(Alignment.Center), + tint = MaterialTheme.colors.primary + ) + } + } + Box( + Modifier + .fillMaxWidth() + .aspectRatio(1f / labelHeightRatio) + .background(labelBg), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Icon( + painterResource(icon), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colors.primary + ) + } + Text( + title, + style = (if (appPlatform.isDesktop) MaterialTheme.typography.h3 else MaterialTheme.typography.h4).copy(fontWeight = FontWeight.Medium), + color = MaterialTheme.colors.onBackground, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + if (subtitle != null) { + Text( + subtitle, + style = if (appPlatform.isDesktop) MaterialTheme.typography.body1 else MaterialTheme.typography.body2, + color = MaterialTheme.colors.onBackground.copy(alpha = 0.7f) + ) + } + } + } + } + } +} + +@Composable +private fun PageHeader(title: String, isLandscape: Boolean, onBack: (() -> Unit)? = null) { + val color = if (onBack != null) MaterialTheme.colors.primary else Color.Transparent + val baseStyle = MaterialTheme.typography.h1 + val titleView = @Composable { + var fontScale by remember(title) { mutableStateOf(1f) } + Text( + title, + style = baseStyle.copy(fontSize = baseStyle.fontSize * fontScale), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + onTextLayout = { result -> + if (result.hasVisualOverflow && fontScale > 0.5f) { + fontScale -= 0.05f + } + } + ) + } + if (isLandscape) { + Box(Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING)) { + BackButton(Modifier.align(Alignment.CenterStart), color, onBack) + titleView() + } + } else { + Column(Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING)) { + Box(Modifier.align(Alignment.Start)) { + BackButton(color = color, onClick = onBack) + } + titleView() + } + } +} + +@Composable +private fun BackButton(modifier: Modifier = Modifier, color: Color = MaterialTheme.colors.primary, onClick: (() -> Unit)? = null) { + Row( + modifier + .clip(RoundedCornerShape(20.dp)) + .clickable(enabled = onClick != null, onClick = onClick ?: {}) + .padding(end = 12.dp, top = 10.dp, bottom = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + painterResource(MR.images.ic_arrow_back_ios_new), + contentDescription = stringResource(MR.strings.back), + tint = color, + modifier = Modifier.height(24.dp) + ) + Text(stringResource(MR.strings.back), color = color) + } +} + +@Composable +private fun CardPair( + isLandscape: Boolean, + heightRatio: Float, + card1: @Composable () -> Unit, + card2: @Composable () -> Unit +) { + if (isLandscape) { + Row( + Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically + ) { + Box(Modifier.weight(1f).maxHeightByWidthRatio(heightRatio)) { card1() } + Box(Modifier.weight(1f).maxHeightByWidthRatio(heightRatio)) { card2() } + } + } else { + Column( + Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING), + verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING, Alignment.CenterVertically) + ) { + Box(Modifier.fillMaxWidth().weight(1f, fill = false).maxHeightByWidthRatio(heightRatio)) { card1() } + Box(Modifier.fillMaxWidth().weight(1f, fill = false).maxHeightByWidthRatio(heightRatio)) { card2() } + } + } +} + +@Composable +private fun OnboardingPageLayout( + title: String, + onBack: (() -> Unit)? = null, + cards: @Composable (isLandscape: Boolean) -> Unit +) { + val isLandscape = appPlatform.isDesktop || windowOrientation() == WindowOrientation.LANDSCAPE + Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) { + PageHeader(title = title, isLandscape = isLandscape, onBack = onBack) + Box(Modifier.weight(1f).fillMaxWidth().padding(vertical = DEFAULT_PADDING)) { + cards(isLandscape) + } + } +} + +@Composable +fun ConnectOnboardingView() { + val pagerState = rememberPagerState(initialPage = 0) { 2 } + val scope = rememberCoroutineScope() + + val startModalsOpen = appPlatform.isDesktop && ModalManager.start.hasModalsOpen + val cardAlpha by animateFloatAsState(if (startModalsOpen) 0.3f else 1f) + + val cardClickOverride: (() -> Unit)? = if (startModalsOpen) { + { ModalManager.start.closeModals() } + } else null + + fun goToPage(target: Int) { + if (appPlatform.isDesktop) { + scope.launch { pagerState.scrollToPage(target) } + } else { + scope.launch { pagerState.animateScrollToPage(target, animationSpec = tween(350)) } + } + } + + val pager = @Composable { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxWidth(), + userScrollEnabled = !appPlatform.isDesktop + ) { page -> + when (page) { + 0 -> OnboardingPageLayout(title = stringResource(MR.strings.talk_to_someone)) { isLandscape -> + CardPair(isLandscape, CARD_HEIGHT_RATIO, + card1 = { + OnboardingCardView( + imageName = MR.images.card_let_someone_connect_to_you_alpha, + imageNameLight = MR.images.card_let_someone_connect_to_you_alpha_light, + icon = MR.images.ic_add_link, + title = stringResource(MR.strings.let_someone_connect_to_you), + labelHeightRatio = 0.132f, + onClick = cardClickOverride ?: { goToPage(1) } + ) + }, + card2 = { + OnboardingCardView( + imageName = MR.images.card_connect_via_link_alpha, + imageNameLight = MR.images.card_connect_via_link_alpha_light, + icon = MR.images.ic_qr_code_scanner, + title = stringResource(MR.strings.connect_via_link_or_qr_code), + labelHeightRatio = 0.132f, + onClick = cardClickOverride ?: { + ModalManager.start.showModalCloseable { close -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, onboarding = true, close = close) + } + } + ) + } + ) + } + 1 -> OnboardingPageLayout( + title = stringResource(MR.strings.connect_with_someone), + onBack = cardClickOverride ?: { goToPage(0) } + ) { isLandscape -> + CardPair(isLandscape, CARD_HEIGHT_RATIO, + card1 = { + OnboardingCardView( + imageName = MR.images.card_invite_someone_privately_alpha, + imageNameLight = MR.images.card_invite_someone_privately_alpha_light, + icon = MR.images.ic_add_link, + title = stringResource(MR.strings.invite_someone_privately), + subtitle = stringResource(MR.strings.a_link_for_one_person), + labelHeightRatio = 0.195f, + onClick = cardClickOverride ?: { + ModalManager.start.showModalCloseable { close -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, onboarding = true, close = close) + } + } + ) + }, + card2 = { + OnboardingCardView( + imageName = MR.images.card_create_your_public_address_alpha, + imageNameLight = MR.images.card_create_your_public_address_alpha_light, + icon = MR.images.ic_qr_code, + title = stringResource(if (chatModel.userAddress.value != null) MR.strings.your_public_address else MR.strings.create_your_public_address), + subtitle = stringResource(MR.strings.for_anyone_to_reach_you), + labelHeightRatio = 0.195f, + onClick = cardClickOverride ?: { + ModalManager.start.showModalCloseable { close -> + UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, onboarding = true, close = close) + } + } + ) + } + ) + } + } + } + } + + if (appPlatform.isDesktop) { + val maxContentWidth = DEFAULT_WINDOW_WIDTH - DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier + Box( + Modifier.fillMaxSize().background(MaterialTheme.colors.background).padding(vertical = DEFAULT_PADDING).graphicsLayer { alpha = cardAlpha }, + contentAlignment = Alignment.Center + ) { + Box(Modifier.widthIn(max = maxContentWidth).fillMaxHeight()) { + pager() + } + } + } else { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + pager() + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt index 9c6c0fa635..2fd77b46a1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt @@ -14,80 +14,160 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.TextStyle - import androidx.compose.ui.text.style.TextAlign +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.common.BuildConfigCommon import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.darkStops +import chat.simplex.common.views.newchat.gradientPoints +import chat.simplex.common.views.newchat.lightStops import chat.simplex.common.views.usersettings.networkAndServers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @Composable -fun ModalData.OnboardingConditionsView() { +fun OnboardingConditionsView(chatModel: ChatModel) { LaunchedEffect(Unit) { prepareChatBeforeFinishingOnboarding() } - CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { - ModalView({}, showClose = false) { - val serverOperators = remember { derivedStateOf { chatModel.conditions.value.serverOperators } } - val selectedOperatorIds = remember { stateGetOrPut("selectedOperatorIds") { serverOperators.value.filter { it.enabled }.map { it.operatorId }.toSet() } } - ColumnWithScrollBar( - Modifier - .themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer), - maxIntrinsicSize = true - ) { - Box(Modifier.align(Alignment.CenterHorizontally)) { - AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), bottomPadding = DEFAULT_PADDING) - } + val serverOperators = remember { derivedStateOf { chatModel.conditions.value.serverOperators } } + val selectedOperatorIds = remember { + mutableStateOf(OnboardingSharedState.selectedOperatorIds.ifEmpty { + serverOperators.value.filter { it.enabled }.map { it.operatorId }.toSet() + }) + } - Spacer(Modifier.weight(1f)) - Column( - (if (appPlatform.isDesktop) Modifier.width(450.dp).align(Alignment.CenterHorizontally) else Modifier) - .fillMaxWidth() - .padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING), - horizontalAlignment = Alignment.Start - ) { - Text( - stringResource(MR.strings.onboarding_conditions_private_chats_not_accessible), - style = TextStyle(fontSize = 17.sp, lineHeight = 23.sp) - ) - Spacer(Modifier.height(DEFAULT_PADDING)) - Text( - stringResource(MR.strings.onboarding_conditions_by_using_you_agree), - style = TextStyle(fontSize = 17.sp, lineHeight = 23.sp) - ) - Spacer(Modifier.height(DEFAULT_PADDING)) - Text( - stringResource(MR.strings.onboarding_conditions_privacy_policy_and_conditions_of_use), - style = TextStyle(fontSize = 17.sp), - color = MaterialTheme.colors.primary, - modifier = Modifier - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null + if (appPlatform.isDesktop) { + OnboardingConditionsDesktop(selectedOperatorIds) + } else { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({}, showClose = false, showAppBar = false) { + OnboardingShrinkingLayout( + modifier = Modifier.fillMaxSize().themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer) + .systemBarsPadding() + .padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING), + topPadding = DEFAULT_PADDING, + image = { + Column(Modifier.padding(vertical = DEFAULT_PADDING_HALF), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingImage( + MR.images.network_commitments, MR.images.network_commitments_light, MR.images.ic_shield, + modifier = Modifier.fillMaxWidth(), + aspectRatio = 1.5f + ) + } + }, + content = { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + stringResource(MR.strings.onboarding_network_commitments), + style = MaterialTheme.typography.h1, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + lineHeight = 42.sp, + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF) + ) + Column( + Modifier.fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING_HALF) + .padding(top = DEFAULT_PADDING), + horizontalAlignment = Alignment.Start ) { - ModalManager.fullscreen.showModal(endButtons = { ConditionsLinkButton() }) { - SimpleConditionsView(rhId = null) - } + Text( + stringResource(MR.strings.onboarding_conditions_private_chats_not_accessible), + style = MaterialTheme.typography.body2, + lineHeight = 22.sp + ) + Spacer(Modifier.height(DEFAULT_PADDING)) + Text( + stringResource(MR.strings.onboarding_conditions_by_using_you_agree), + style = MaterialTheme.typography.body2, + lineHeight = 22.sp + ) + Spacer(Modifier.height(DEFAULT_PADDING)) + Text( + stringResource(MR.strings.onboarding_conditions_privacy_policy_and_conditions_of_use), + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colors.primary, + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + ModalManager.fullscreen.showModal(endButtons = { ConditionsLinkButton() }) { + SimpleConditionsView(rhId = null) { + ModalManager.fullscreen.closeModal() + acceptConditions(selectedOperatorIds.value) + } + } + } + ) } - ) - } - Spacer(Modifier.weight(1f)) - - Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { - AcceptConditionsButton(enabled = selectedOperatorIds.value.isNotEmpty(), selectedOperatorIds) - TextButtonBelowOnboardingButton(stringResource(MR.strings.onboarding_conditions_configure_server_operators)) { - ModalManager.fullscreen.showModalCloseable { close -> - ChooseServerOperators(serverOperators, selectedOperatorIds, close) + } + }, + button = { + Column(Modifier.widthIn(max = 450.dp).padding(bottom = DEFAULT_PADDING * 2), horizontalAlignment = Alignment.CenterHorizontally) { + AcceptConditionsButton(enabled = selectedOperatorIds.value.isNotEmpty(), selectedOperatorIds) } } + ) + } + } + } +} + +@Composable +private fun OnboardingConditionsDesktop(selectedOperatorIds: MutableState>) { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({}, showClose = false) { + ColumnWithScrollBar(horizontalAlignment = Alignment.CenterHorizontally) { + Column(Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + Box(Modifier.align(Alignment.CenterHorizontally)) { + AppBarTitle(stringResource(MR.strings.onboarding_network_commitments), bottomPadding = DEFAULT_PADDING, withPadding = false, overrideTitleColor = MaterialTheme.colors.onBackground, textAlign = TextAlign.Center, lineHeight = 42.sp) + } + Column(Modifier.width(450.dp), horizontalAlignment = Alignment.Start) { + ReadableText(MR.strings.onboarding_conditions_private_chats_not_accessible, TextAlign.Start, padding = PaddingValues(), style = MaterialTheme.typography.body1) + Spacer(Modifier.height(DEFAULT_PADDING)) + ReadableText(MR.strings.onboarding_conditions_by_using_you_agree, TextAlign.Start, padding = PaddingValues(), style = MaterialTheme.typography.body1) + Spacer(Modifier.height(DEFAULT_PADDING)) + Text( + stringResource(MR.strings.onboarding_conditions_privacy_policy_and_conditions_of_use), + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colors.primary, + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + ModalManager.fullscreen.showModal(forceAnimated = true, endButtons = { ConditionsLinkButton() }) { + SimpleConditionsView(rhId = null) { + ModalManager.fullscreen.closeModal() + acceptConditions(selectedOperatorIds.value) + } + } + } + ) + } + } + Spacer(Modifier.fillMaxHeight().weight(1f)) + Column(Modifier.widthIn(max = 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + AcceptConditionsButton(enabled = selectedOperatorIds.value.isNotEmpty(), selectedOperatorIds) + TextButtonBelowOnboardingButton("", null) } } } @@ -104,7 +184,7 @@ fun ModalData.ChooseServerOperators( prepareChatBeforeFinishingOnboarding() } CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { - ModalView({}, showClose = false) { + ModalView(close, enableClose = selectedOperatorIds.value.isNotEmpty()) { ColumnWithScrollBar( Modifier .themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer), @@ -141,11 +221,9 @@ fun ModalData.ChooseServerOperators( } Spacer(Modifier.weight(1f)) - Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).padding(bottom = DEFAULT_PADDING * 2).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { val enabled = selectedOperatorIds.value.isNotEmpty() SetOperatorsButton(enabled, close) - // Reserve space - TextButtonBelowOnboardingButton("", null) } } } @@ -212,52 +290,42 @@ private fun SetOperatorsButton(enabled: Boolean, close: () -> Unit) { ) } +private fun acceptConditions(selectedOperatorIds: Set) { + withBGApi { + val conditionsId = chatModel.conditions.value.currentConditions.conditionsId + val r = chatController.acceptConditions(chatModel.remoteHostId(), conditionsId = conditionsId, operatorIds = selectedOperatorIds.toList()) + if (r != null) { + chatModel.conditions.value = r + val enabledOps = enabledOperators(r.serverOperators, selectedOperatorIds) + if (enabledOps != null) { + val r2 = chatController.setServerOperators(rh = chatModel.remoteHostId(), operators = enabledOps) + if (r2 != null) { + chatModel.conditions.value = r2 + completeOnboarding() + } + } else { + completeOnboarding() + } + } + } +} + @Composable private fun AcceptConditionsButton( enabled: Boolean, selectedOperatorIds: State> ) { - fun continueOnAccept() { - if (appPlatform.isDesktop) { - continueToNextStep() - } else { - continueToSetNotificationsAfterAccept() - } - } OnboardingActionButton( modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), labelId = MR.strings.onboarding_conditions_accept, onboarding = null, enabled = enabled, - onclick = { - withBGApi { - val conditionsId = chatModel.conditions.value.currentConditions.conditionsId - val r = chatController.acceptConditions(chatModel.remoteHostId(), conditionsId = conditionsId, operatorIds = selectedOperatorIds.value.toList()) - if (r != null) { - chatModel.conditions.value = r - val enabledOperators = enabledOperators(r.serverOperators, selectedOperatorIds.value) - if (enabledOperators != null) { - val r2 = chatController.setServerOperators(rh = chatModel.remoteHostId(), operators = enabledOperators) - if (r2 != null) { - chatModel.conditions.value = r2 - continueOnAccept() - } - } else { - continueOnAccept() - } - } - } - } + onclick = { acceptConditions(selectedOperatorIds.value) } ) } -private fun continueToNextStep() { - appPrefs.onboardingStage.set(if (appPlatform.isAndroid) OnboardingStage.Step4_SetNotificationsMode else OnboardingStage.OnboardingComplete) -} - -private fun continueToSetNotificationsAfterAccept() { - appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode) - ModalManager.fullscreen.showModalCloseable(showClose = false) { SetNotificationsMode(chatModel) } +private fun completeOnboarding() { + appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) } private fun enabledOperators(operators: List, selectedOperatorIds: Set): List? { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt index aff02e90f5..703d295523 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt @@ -1,8 +1,7 @@ package chat.simplex.common.views.onboarding import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -15,7 +14,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* -import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.helpers.* @@ -24,21 +23,26 @@ import dev.icerock.moko.resources.StringResource @Composable fun HowItWorks(user: User?, onboardingStage: SharedPreference? = null) { - ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false) - ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues) - ReadableText(MR.strings.only_client_devices_store_contacts_groups_e2e_encrypted_messages) - ReadableText(MR.strings.all_message_and_files_e2e_encrypted) - if (onboardingStage == null) { - ReadableTextWithLink(MR.strings.read_more_in_github_with_link, "https://github.com/simplex-chat/simplex-chat#readme") + Column(Modifier.fillMaxSize().padding(horizontal = if (appPlatform.isDesktop) DEFAULT_PADDING * 2 else DEFAULT_PADDING)) { + Spacer(Modifier.statusBarsPadding().padding(top = AppBarHeight * fontSizeSqrtMultiplier)) + val paraPadding = PaddingValues(bottom = if (appPlatform.isDesktop) 10.dp else 12.dp) + Column(Modifier.weight(1f).padding(bottom = DEFAULT_PADDING).verticalScroll(rememberScrollState())) { + Text(stringResource(MR.strings.why_built_heading), style = MaterialTheme.typography.h1, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) + ReadableText(MR.strings.why_built_p1, padding = paraPadding) + ReadableText(MR.strings.why_built_p2, padding = paraPadding) + ReadableText(MR.strings.why_built_p3, padding = paraPadding) + ReadableText(MR.strings.why_built_p4, padding = paraPadding) + ReadableText(MR.strings.why_built_p5, padding = paraPadding) + ReadableText(MR.strings.why_built_p6, padding = paraPadding) + ReadableText(MR.strings.why_built_p7, padding = paraPadding) + ReadableText(MR.strings.why_built_tagline, padding = paraPadding) } - - Spacer(Modifier.fillMaxHeight().weight(1f)) - if (onboardingStage != null) { - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Column( + Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), + horizontalAlignment = Alignment.CenterHorizontally + ) { OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.fullscreen.closeModal() }) - // Reserve space TextButtonBelowOnboardingButton("", null) } } @@ -67,7 +71,7 @@ fun ReadableTextWithLink(stringResId: StringResource, link: String, textAlign: T newStyles } val uriHandler = LocalUriHandler.current - Text(AnnotatedString(annotated.text, newStyles), modifier = Modifier.padding(padding).clickable { if (simplexLink) uriHandler.openVerifiedSimplexUri(link) else 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.openExternalLink(link) }, textAlign = textAlign, lineHeight = 22.sp) } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt index 9e48f4b2bd..e902b7947e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt @@ -3,6 +3,7 @@ package chat.simplex.common.views.onboarding import SectionTextFooter import SectionView import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment @@ -57,7 +58,7 @@ private fun LinkAMobileLayout( ModalView({ appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) }) { Column(Modifier.fillMaxSize().padding(top = AppBarHeight * fontSizeSqrtMultiplier)) { Box(Modifier.align(Alignment.CenterHorizontally)) { - AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) + AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), overrideTitleColor = MaterialTheme.colors.onBackground) } Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) { Column( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingLayout.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingLayout.kt new file mode 100644 index 0000000000..684bfb0053 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingLayout.kt @@ -0,0 +1,161 @@ +package chat.simplex.common.views.onboarding + +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.Divider +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.layout.* +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import chat.simplex.common.BuildConfigCommon +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.ui.theme.isInDarkTheme +import chat.simplex.common.views.helpers.ModalManager +import chat.simplex.common.views.helpers.mixWith +import chat.simplex.common.views.newchat.darkStops +import chat.simplex.common.views.newchat.gradientPoints +import chat.simplex.common.views.newchat.lightStops +import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource +import dev.icerock.moko.resources.compose.painterResource + +/** + * A layout for onboarding screens: image + content + spacer + button. + * The spacer shrinks first (down to [minSpacerHeight]), then the image shrinks. + * Button is always at the bottom. + */ +@Composable +fun OnboardingShrinkingLayout( + modifier: Modifier = Modifier, + topPadding: Dp = 0.dp, + minSpacerHeight: Dp = 20.dp, + image: @Composable () -> Unit, + content: @Composable () -> Unit, + button: @Composable () -> Unit +) { + Layout( + contents = listOf(image, content, button), + modifier = modifier + ) { (imageMeasurables, contentMeasurables, buttonMeasurables), constraints -> + val width = constraints.maxWidth + val height = constraints.maxHeight + val childConstraints = constraints.copy(minWidth = 0, minHeight = 0) + + // 1. Measure fixed content (texts) and button first + val contentPlaceable = contentMeasurables.first().measure(childConstraints) + val buttonPlaceable = buttonMeasurables.first().measure(childConstraints) + val minSpacer = minSpacerHeight.roundToPx() + + // 2. Image gets remaining after top padding + content + button + minimum spacer + val topPad = topPadding.roundToPx() + val reservedHeight = topPad + contentPlaceable.height + buttonPlaceable.height + minSpacer + val imageMaxHeight = (height - reservedHeight).coerceAtLeast(0) + val imagePlaceable = imageMeasurables.first().measure( + childConstraints.copy(maxWidth = width, maxHeight = imageMaxHeight) + ) + + // 3. Spacer fills whatever is left between content and button + val usedHeight = topPad + imagePlaceable.height + contentPlaceable.height + buttonPlaceable.height + val spacerHeight = (height - usedHeight).coerceAtLeast(minSpacer) + + // 4. Place: image centered horizontally, rest below + layout(width, height) { + var y = topPad + imagePlaceable.placeRelative((width - imagePlaceable.width) / 2, y) + y += imagePlaceable.height + contentPlaceable.placeRelative((width - contentPlaceable.width) / 2, y) + y += contentPlaceable.height + y += spacerHeight + buttonPlaceable.placeRelative((width - buttonPlaceable.width) / 2, y) + } + } +} + +@Composable +fun OnboardingImage( + lightImage: ImageResource, + darkImage: ImageResource, + fallbackIcon: ImageResource, + modifier: Modifier = Modifier, + aspectRatio: Float = 1f +) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isInDarkTheme()) darkImage else lightImage), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxWidth().then(modifier) + ) + } else { + val isDark = isInDarkTheme() + val stops = if (isDark) darkStops else lightStops + val scale = if (isDark) 1.5f else 1.2f + Box( + modifier + .aspectRatio(aspectRatio) + .clip(RoundedCornerShape(24.dp)) + .drawBehind { + val gp = gradientPoints(size.height / size.width, scale) + drawRect( + Brush.linearGradient( + colorStops = stops, + start = Offset(gp.startX * size.width, gp.startY * size.height), + end = Offset(gp.endX * size.width, gp.endY * size.height) + ) + ) + }, + contentAlignment = Alignment.Center + ) { + Icon( + painterResource(fallbackIcon), + contentDescription = null, + modifier = Modifier.size(80.dp), + tint = MaterialTheme.colors.primary + ) + } + } +} + +@Composable +fun DesktopOnboardingShell(stage: OnboardingStage, content: @Composable () -> Unit) { + Row(Modifier.fillMaxSize()) { + Box( + Modifier.weight(0.382f).fillMaxHeight() + .background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.985f)) + .padding(horizontal = DEFAULT_PADDING), + contentAlignment = Alignment.Center + ) { + when (stage) { + OnboardingStage.Step1_SimpleXInfo -> + OnboardingImage(MR.images.intro, MR.images.intro_light, MR.images.ic_forum, Modifier.fillMaxWidth()) + OnboardingStage.Step2_CreateProfile, + OnboardingStage.Step2_5_SetupDatabasePassphrase, + OnboardingStage.LinkAMobile -> + OnboardingImage(MR.images.your_profile, MR.images.your_profile_light, MR.images.ic_person, Modifier.fillMaxWidth()) + OnboardingStage.Step3_ChooseServerOperators, + OnboardingStage.Step3_CreateSimpleXAddress, + OnboardingStage.Step4_SetNotificationsMode -> + OnboardingImage(MR.images.your_network, MR.images.your_network_light, MR.images.ic_dns, Modifier.fillMaxWidth()) + OnboardingStage.Step4_NetworkCommitments -> + OnboardingImage(MR.images.network_commitments, MR.images.network_commitments_light, MR.images.ic_shield, Modifier.fillMaxWidth(), aspectRatio = 1.5f) + else -> {} + } + } + Divider(Modifier.fillMaxHeight().width(1.dp)) + Box(Modifier.weight(0.618f).fillMaxHeight().clipToBounds()) { + content() + ModalManager.fullscreen.showInView() + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt index 510df13c3d..7af364b855 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt @@ -6,7 +6,8 @@ enum class OnboardingStage { LinkAMobile, Step2_5_SetupDatabasePassphrase, Step3_ChooseServerOperators, - Step3_CreateSimpleXAddress, - Step4_SetNotificationsMode, + Step3_CreateSimpleXAddress, // deprecated + Step4_SetNotificationsMode, // deprecated + Step4_NetworkCommitments, OnboardingComplete } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt index 84f473067f..adcfb8b194 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt @@ -5,8 +5,8 @@ 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 import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import dev.icerock.moko.resources.compose.stringResource @@ -14,27 +14,21 @@ 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.common.model.ChatModel import chat.simplex.common.model.NotificationsMode import chat.simplex.common.platform.* 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.compose.painterResource @Composable -fun SetNotificationsMode(m: ChatModel) { - LaunchedEffect(Unit) { - prepareChatBeforeFinishingOnboarding() - } - +fun SetNotificationsMode(currentMode: MutableState, onDone: () -> Unit) { CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { ModalView({}, showClose = false) { ColumnWithScrollBar(Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { Box(Modifier.align(Alignment.CenterHorizontally)) { AppBarTitle(stringResource(MR.strings.onboarding_notifications_mode_title), bottomPadding = DEFAULT_PADDING) } - val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) } Column(Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { OnboardingInformationButton( stringResource(MR.strings.onboarding_notifications_mode_subtitle), @@ -43,34 +37,28 @@ fun SetNotificationsMode(m: ChatModel) { } Spacer(Modifier.weight(1f)) Column(Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING)) { - SelectableCard(currentMode, NotificationsMode.SERVICE, stringResource(MR.strings.onboarding_notifications_mode_service), annotatedStringResource(MR.strings.onboarding_notifications_mode_service_desc_short)) { + SelectableCard(currentMode, NotificationsMode.SERVICE, stringResource(MR.strings.onboarding_notifications_mode_service), annotatedStringResource(MR.strings.onboarding_notifications_mode_service_desc_short), icon = painterResource(MR.images.ic_bolt)) { currentMode.value = NotificationsMode.SERVICE } - SelectableCard(currentMode, NotificationsMode.PERIODIC, stringResource(MR.strings.onboarding_notifications_mode_periodic), annotatedStringResource(MR.strings.onboarding_notifications_mode_periodic_desc_short)) { + SelectableCard(currentMode, NotificationsMode.PERIODIC, stringResource(MR.strings.onboarding_notifications_mode_periodic), annotatedStringResource(MR.strings.onboarding_notifications_mode_periodic_desc_short), icon = painterResource(MR.images.ic_timer)) { currentMode.value = NotificationsMode.PERIODIC } - SelectableCard(currentMode, NotificationsMode.OFF, stringResource(MR.strings.onboarding_notifications_mode_off), annotatedStringResource(MR.strings.onboarding_notifications_mode_off_desc_short)) { + SelectableCard(currentMode, NotificationsMode.OFF, stringResource(MR.strings.onboarding_notifications_mode_off), annotatedStringResource(MR.strings.onboarding_notifications_mode_off_desc_short), icon = painterResource(MR.images.ic_bolt_off)) { currentMode.value = NotificationsMode.OFF } } Spacer(Modifier.weight(1f)) - Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).padding(bottom = DEFAULT_PADDING * 2).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { OnboardingActionButton( modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier, - labelId = MR.strings.use_chat, - onboarding = OnboardingStage.OnboardingComplete, - onclick = { - changeNotificationsMode(currentMode.value, m) - ModalManager.fullscreen.closeModals() - } + labelId = MR.strings.ok, + onboarding = null, + onclick = onDone ) - // Reserve space - TextButtonBelowOnboardingButton("", null) } } } } - SetNotificationsModeAdditions() } @Composable @@ -78,20 +66,31 @@ expect fun SetNotificationsModeAdditions() @Composable fun SelectableCard(currentValue: State, newValue: T, title: String, description: AnnotatedString, onSelected: (T) -> Unit) { + SelectableCard(currentValue, newValue, title, description, icon = null, onSelected) +} + +@Composable +fun SelectableCard(currentValue: State, newValue: T, title: String, description: AnnotatedString, icon: Painter?, onSelected: (T) -> Unit) { + val titleColor = if (currentValue.value == newValue) MaterialTheme.colors.primary else MaterialTheme.colors.secondary TextButton( onClick = { onSelected(newValue) }, border = BorderStroke(1.dp, color = if (currentValue.value == newValue) MaterialTheme.colors.primary else MaterialTheme.colors.secondary.copy(alpha = 0.5f)), shape = RoundedCornerShape(35.dp), ) { Column(Modifier.padding(horizontal = 10.dp).padding(top = 4.dp, bottom = 8.dp).fillMaxWidth()) { - Text( - title, - style = MaterialTheme.typography.h3, - fontWeight = FontWeight.Medium, - color = if (currentValue.value == newValue) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, - modifier = Modifier.padding(bottom = 8.dp).align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center - ) + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(bottom = 8.dp).align(Alignment.CenterHorizontally)) { + if (icon != null) { + Icon(icon, null, Modifier.size(18.dp), tint = titleColor) + Spacer(Modifier.width(8.dp)) + } + Text( + title, + style = MaterialTheme.typography.h3, + fontWeight = FontWeight.Medium, + color = titleColor, + textAlign = TextAlign.Center + ) + } Text(description, Modifier.align(Alignment.CenterHorizontally), fontSize = 15.sp, @@ -105,7 +104,7 @@ fun SelectableCard(currentValue: State, newValue: T, title: String, descr } @Composable -private fun NotificationBatteryUsageInfo() { +fun NotificationBatteryUsageInfo() { ColumnWithScrollBar(Modifier.padding(DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.onboarding_notifications_mode_battery), withPadding = false) Text(stringResource(MR.strings.onboarding_notifications_mode_service), style = MaterialTheme.typography.h3, color = MaterialTheme.colors.secondary) 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 index c6eceb0ce2..9ef72a7f12 100644 --- 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 @@ -107,7 +107,7 @@ private fun SetupDatabasePassphraseLayout( Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer).padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally, ) { - AppBarTitle(stringResource(MR.strings.setup_database_passphrase)) + AppBarTitle(stringResource(MR.strings.setup_database_passphrase), overrideTitleColor = MaterialTheme.colors.onBackground) val onClickUpdate = { // Don't do things concurrently. Shouldn't be here concurrently, just in case diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt index e5d00fddd1..74dadcd671 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt @@ -11,6 +11,9 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale @@ -21,11 +24,15 @@ 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.unit.* +import chat.simplex.common.BuildConfigCommon import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.darkStops +import chat.simplex.common.views.newchat.gradientPoints +import chat.simplex.common.views.newchat.lightStops import chat.simplex.common.views.migration.MigrateToDeviceView import chat.simplex.common.views.migration.MigrationToState import chat.simplex.res.MR @@ -36,12 +43,16 @@ import kotlin.math.floor @Composable fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) { if (onboarding) { - CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { - ModalView({}, showClose = false, showAppBar = false) { - SimpleXInfoLayout( - user = chatModel.currentUser.value, - onboardingStage = chatModel.controller.appPrefs.onboardingStage - ) + if (appPlatform.isDesktop) { + SimpleXInfoDesktop(chatModel) + } else { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({}, showClose = false, showAppBar = false) { + SimpleXInfoLayout( + user = chatModel.currentUser.value, + onboardingStage = chatModel.controller.appPrefs.onboardingStage + ) + } } } } else { @@ -52,40 +63,106 @@ fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) { } } +@Composable +private fun SimpleXInfoDesktop(chatModel: ChatModel) { + val user = chatModel.currentUser.value + val onboardingStage = chatModel.controller.appPrefs.onboardingStage + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({}, showClose = false) { + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(Modifier.height(DEFAULT_PADDING)) + Box(Modifier.widthIn(max = 600.dp).fillMaxWidth(0.45f).align(Alignment.CenterHorizontally)) { + SimpleXLogo() + } + Spacer(Modifier.fillMaxHeight().weight(1f)) + Column(Modifier.widthIn(max = 600.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + Box(Modifier.align(Alignment.CenterHorizontally)) { + AppBarTitle(stringResource(MR.strings.onboarding_be_free), bottomPadding = DEFAULT_PADDING, withPadding = false, overrideTitleColor = MaterialTheme.colors.onBackground, textAlign = TextAlign.Center, lineHeight = 42.sp) + } + Text(stringResource(MR.strings.onboarding_private_and_secure), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Medium, color = MaterialTheme.colors.secondary, lineHeight = 25.sp, textAlign = TextAlign.Center) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + ReadableText(MR.strings.onboarding_first_network, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.secondary)) + } + Spacer(Modifier.fillMaxHeight().weight(1f)) + Column(Modifier.widthIn(max = 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingActionButton(user, onboardingStage) + TextButtonBelowOnboardingButton(stringResource(MR.strings.why_simplex_is_built), icon = painterResource(MR.images.ic_info), onClick = { + ModalManager.fullscreen.showModal(forceAnimated = true) { HowItWorks(user, onboardingStage) } + }) + } + } + } + } + LaunchedEffect(Unit) { + if (chatModel.migrationState.value != null && !ModalManager.fullscreen.hasModalsOpen()) { + ModalManager.fullscreen.showCustomModal(animated = false) { close -> MigrateToDeviceView(close) } + } + } +} + @Composable fun SimpleXInfoLayout( user: User?, onboardingStage: SharedPreference? ) { - ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING), horizontalAlignment = Alignment.CenterHorizontally) { - Box(Modifier.widthIn(max = if (appPlatform.isAndroid) 250.dp else 500.dp).padding(top = DEFAULT_PADDING + 8.dp), contentAlignment = Alignment.Center) { + val topBar = onboardingStage == null && !appPrefs.oneHandUI.state.value + val modifier = Modifier.fillMaxSize().systemBarsPadding().padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING) + Column(if (topBar) modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier) else modifier, horizontalAlignment = Alignment.CenterHorizontally) { + Box(Modifier.padding(top = DEFAULT_PADDING * 2).widthIn(max = if (appPlatform.isAndroid) 185.dp else 160.dp), contentAlignment = Alignment.Center) { SimpleXLogo() } - - OnboardingInformationButton( - stringResource(MR.strings.next_generation_of_private_messaging), - onClick = { ModalManager.fullscreen.showModal { HowItWorks(user, onboardingStage) } }, - ) - - Spacer(Modifier.weight(1f)) - - Column { - InfoRow(painterResource(MR.images.privacy), MR.strings.privacy_redefined, MR.strings.first_platform_without_user_ids, width = 60.dp) - InfoRow(painterResource(MR.images.shield), MR.strings.immune_to_spam_and_abuse, MR.strings.people_can_connect_only_via_links_you_share, width = 46.dp) - 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(Modifier.fillMaxHeight().weight(1f)) { } - - if (onboardingStage != null) { - Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally,) { - OnboardingActionButton(user, onboardingStage) - TextButtonBelowOnboardingButton(stringResource(MR.strings.migrate_from_another_device)) { - chatModel.migrationState.value = MigrationToState.PasteOrScanLink - ModalManager.fullscreen.showCustomModal { close -> MigrateToDeviceView(close) } + OnboardingShrinkingLayout( + modifier = Modifier.fillMaxSize(), + image = { + Column(Modifier.padding(vertical = DEFAULT_PADDING_HALF), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingImage( + MR.images.intro, MR.images.intro_light, MR.images.ic_forum, + modifier = if (appPlatform.isAndroid) Modifier.fillMaxWidth() else Modifier.heightIn(max = 280.dp) + ) + } + }, + content = { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + stringResource(MR.strings.onboarding_be_free), + style = MaterialTheme.typography.h1, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + lineHeight = 42.sp, + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF) + ) + Text( + stringResource(MR.strings.onboarding_private_and_secure), + style = MaterialTheme.typography.h3, + color = MaterialTheme.colors.secondary, + fontWeight = FontWeight.Medium, + lineHeight = 25.sp, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 14.dp) + ) + Text( + stringResource(MR.strings.onboarding_first_network), + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.secondary, + textAlign = TextAlign.Center, + lineHeight = 20.sp, + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF) + ) + } + }, + button = { + if (onboardingStage != null) { + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingActionButton(user, onboardingStage) + TextButtonBelowOnboardingButton(stringResource(MR.strings.why_simplex_is_built), icon = painterResource(MR.images.ic_info), onClick = { + ModalManager.fullscreen.showModal { HowItWorks(user, onboardingStage) } + }) } + } else { + Spacer(Modifier) } } + ) } LaunchedEffect(Unit) { if (chatModel.migrationState.value != null && !ModalManager.fullscreen.hasModalsOpen()) { @@ -101,25 +178,11 @@ fun SimpleXLogo() { contentDescription = stringResource(MR.strings.image_descr_simplex_logo), contentScale = ContentScale.FillWidth, modifier = Modifier - .padding(vertical = DEFAULT_PADDING) + .padding(bottom = 10.dp) .fillMaxWidth() ) } -@Composable -private fun InfoRow(icon: Painter, titleId: StringResource, textId: StringResource, width: Dp = 58.dp) { - Row(Modifier.padding(bottom = 27.dp), verticalAlignment = Alignment.Top) { - Spacer(Modifier.width((4.dp + 58.dp - width) / 2)) - Image(icon, contentDescription = null, modifier = Modifier - .width(width)) - Spacer(Modifier.width((4.dp + 58.dp - width) / 2 + DEFAULT_PADDING_HALF + 7.dp)) - Column(Modifier.padding(top = 4.dp), verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF)) { - Text(stringResource(titleId), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.h3, lineHeight = 24.sp) - Text(stringResource(textId), lineHeight = 24.sp, style = MaterialTheme.typography.body1, color = MaterialTheme.colors.secondary) - } - } -} - @Composable expect fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference, onclick: (() -> Unit)? = null) @@ -155,16 +218,20 @@ fun OnboardingActionButton( } @Composable -fun TextButtonBelowOnboardingButton(text: String, onClick: (() -> Unit)?) { +fun TextButtonBelowOnboardingButton(text: String, onClick: (() -> Unit)?, icon: Painter? = null) { val state = getKeyboardState() val enabled = onClick != null val topPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else 7.5.dp) val bottomPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else 7.5.dp) if ((appPlatform.isAndroid && state.value == KeyboardState.Closed) || topPadding > 0.dp) { TextButton({ onClick?.invoke() }, Modifier.padding(top = topPadding, bottom = bottomPadding).clip(CircleShape), enabled = enabled) { + if (icon != null) { + Icon(icon, null, tint = MaterialTheme.colors.primary) + Spacer(Modifier.width(4.dp)) + } Text( text, - Modifier.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING_HALF, bottom = 5.dp), + Modifier.padding(vertical = 5.dp), color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center @@ -219,6 +286,7 @@ fun OnboardingInformationButton( textLayoutResult = it }, style = MaterialTheme.typography.button, + fontWeight = FontWeight.Medium, color = MaterialTheme.colors.primary ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index f64f1dcecd..e1415d071d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -59,7 +59,7 @@ fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Bool Icon( painterResource(MR.images.ic_open_in_new), stringResource(titleId), tint = MaterialTheme.colors.primary, modifier = Modifier - .clickable { if (link.startsWith("simplex:")) uriHandler.openVerifiedSimplexUri(link) else uriHandler.openUriCatching(link) } + .clickable { if (link.startsWith("simplex:")) uriHandler.openVerifiedSimplexUri(link) else uriHandler.openExternalLink(link) } ) } @@ -229,7 +229,7 @@ fun ReadMoreButton(url: String) { interactionSource = remember { MutableInteractionSource() }, indication = null ) { - uriHandler.openUriCatching(url) + uriHandler.openExternalLink(url) } ) Icon(painterResource(MR.images.ic_open_in_new), stringResource(MR.strings.whats_new_read_more), tint = MaterialTheme.colors.primary) @@ -880,6 +880,38 @@ private val versionDescriptions: List = listOf( ), ) ), + VersionDescription( + version = "v6.5", + post = "https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html", + features = listOf( + VersionFeature.FeatureDescription( + icon = null, + titleId = MR.strings.v6_5_public_channels, + descrId = null, + subfeatures = listOf( + MR.images.ic_wifi_tethering to MR.strings.v6_5_reliability, + MR.images.ic_dns to MR.strings.v6_5_ownership, + MR.images.ic_vpn_key_filled to MR.strings.v6_5_security, + MR.images.ic_shield to MR.strings.v6_5_privacy, + ) + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_add_link, + titleId = MR.strings.v6_5_invite_friends, + descrId = MR.strings.v6_5_invite_friends_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_security, + titleId = MR.strings.v6_5_safe_web_links, + descrId = MR.strings.v6_5_safe_web_links_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_verified_user, + titleId = MR.strings.v6_5_non_profit_governance, + descrId = MR.strings.v6_5_non_profit_governance_descr + ), + ) + ), ) private val lastVersion = versionDescriptions.last().version diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/YourNetwork.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/YourNetwork.kt new file mode 100644 index 0000000000..b20cfe3096 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/YourNetwork.kt @@ -0,0 +1,226 @@ +package chat.simplex.common.views.onboarding + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +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.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix +import androidx.compose.ui.layout.ContentScale +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.common.BuildConfigCommon +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.darkStops +import chat.simplex.common.views.newchat.gradientPoints +import chat.simplex.common.views.newchat.lightStops +import chat.simplex.common.views.usersettings.changeNotificationsMode +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +internal object OnboardingSharedState { + var selectedOperatorIds: Set = emptySet() +} + +@Composable +fun YourNetworkView(chatModel: ChatModel) { + LaunchedEffect(Unit) { + prepareChatBeforeFinishingOnboarding() + } + + val serverOperators = remember { derivedStateOf { chatModel.conditions.value.serverOperators } } + val selectedOperatorIds = remember { + mutableStateOf(serverOperators.value.filter { it.enabled }.map { it.operatorId }.toSet()) + } + + LaunchedEffect(selectedOperatorIds.value) { + OnboardingSharedState.selectedOperatorIds = selectedOperatorIds.value + } + + val notificationMode = rememberSaveable { mutableStateOf(NotificationsMode.default) } + + if (appPlatform.isDesktop) { + YourNetworkDesktop(serverOperators, selectedOperatorIds) + } else { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({}, showClose = false, showAppBar = false) { + OnboardingShrinkingLayout( + modifier = Modifier.fillMaxSize().themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer) + .systemBarsPadding() + .padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING), + topPadding = DEFAULT_PADDING, + image = { + Column(Modifier.padding(vertical = DEFAULT_PADDING_HALF), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingImage( + MR.images.your_network, MR.images.your_network_light, MR.images.ic_dns, + modifier = Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() + ) + } + }, + content = { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + stringResource(MR.strings.onboarding_your_network), + style = MaterialTheme.typography.h1, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + lineHeight = 42.sp, + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF) + ) + Text( + stringResource(MR.strings.onboarding_network_routers_cannot_know), + style = MaterialTheme.typography.h3, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colors.secondary, + lineHeight = 25.sp, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 14.dp) + ) + Column( + Modifier.padding(top = DEFAULT_PADDING_HALF), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + ConfigureRoutersButton(serverOperators, selectedOperatorIds) { + ModalManager.fullscreen.showCustomModal { close -> + ChooseServerOperators(serverOperators, selectedOperatorIds, close) + } + } + ConfigureNotificationsButton(notificationMode) { + ModalManager.fullscreen.showModalCloseable { close -> + SetNotificationsMode(notificationMode, close) + } + } + } + } + }, + button = { + Column( + Modifier.widthIn(max = 450.dp).padding(bottom = DEFAULT_PADDING * 2), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OnboardingActionButton( + modifier = Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth(), + labelId = MR.strings.onboarding_network_operators_continue, + onboarding = null, + onclick = { + changeNotificationsMode(notificationMode.value, chatModel) + appPrefs.onboardingStage.set(OnboardingStage.Step4_NetworkCommitments) + } + ) + } + } + ) + } + } + } +} + +@Composable +private fun YourNetworkDesktop( + serverOperators: State>, + selectedOperatorIds: MutableState> +) { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({}, showClose = false) { + ColumnWithScrollBar(horizontalAlignment = Alignment.CenterHorizontally) { + Column(Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + Box(Modifier.align(Alignment.CenterHorizontally)) { + AppBarTitle(stringResource(MR.strings.onboarding_your_network), bottomPadding = DEFAULT_PADDING, withPadding = false, overrideTitleColor = MaterialTheme.colors.onBackground, textAlign = TextAlign.Center, lineHeight = 42.sp) + } + Text(stringResource(MR.strings.onboarding_network_routers_cannot_know), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Medium, color = MaterialTheme.colors.secondary, lineHeight = 25.sp, textAlign = TextAlign.Center) + Spacer(Modifier.height(DEFAULT_PADDING)) + ConfigureRoutersButton(serverOperators, selectedOperatorIds) { + ModalManager.fullscreen.showCustomModal(forceAnimated = true) { close -> + ChooseServerOperators(serverOperators, selectedOperatorIds, close) + } + } + } + Spacer(Modifier.fillMaxHeight().weight(1f)) + Column(Modifier.widthIn(max = 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingActionButton( + Modifier.widthIn(min = 300.dp), + labelId = MR.strings.onboarding_network_operators_continue, + onboarding = null, + onclick = { + appPrefs.onboardingStage.set(OnboardingStage.Step4_NetworkCommitments) + } + ) + TextButtonBelowOnboardingButton("", null) + } + } + } + } +} + +@Composable +private fun ConfigureRoutersButton(serverOperators: State>, selectedOperatorIds: State>, onClick: () -> Unit) { + Box( + modifier = Modifier + .clip(CircleShape) + .clickable { onClick() } + ) { + Row(Modifier.padding(8.dp), horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically) { + Text( + stringResource(MR.strings.onboarding_configure_routers), + style = MaterialTheme.typography.button, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colors.primary + ) + serverOperators.value.forEach { op -> + Image( + painterResource(op.logo), + contentDescription = null, + modifier = Modifier.size(22.dp), + colorFilter = if (selectedOperatorIds.value.contains(op.operatorId)) null else ColorFilter.colorMatrix(ColorMatrix().apply { + setToSaturation(0f) + }) + ) + } + } + } +} + +@Composable +private fun ConfigureNotificationsButton(notificationMode: State, onClick: () -> Unit) { + val icon = when (notificationMode.value) { + NotificationsMode.SERVICE -> MR.images.ic_bolt + NotificationsMode.PERIODIC -> MR.images.ic_timer + NotificationsMode.OFF -> MR.images.ic_bolt_off + } + Box( + modifier = Modifier + .clip(CircleShape) + .clickable { onClick() } + ) { + Row(Modifier.padding(8.dp), horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically) { + Text( + stringResource(MR.strings.onboarding_configure_notifications), + style = MaterialTheme.typography.button, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colors.primary + ) + Icon( + painterResource(icon), + contentDescription = null, + tint = MaterialTheme.colors.primary + ) + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/RTCServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/RTCServers.kt index 761a74d6e4..0c31b062dd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/RTCServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/RTCServers.kt @@ -198,7 +198,7 @@ private fun howToButton() { val uriHandler = LocalUriHandler.current Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.clickable { uriHandler.openUriCatching("https://simplex.chat/docs/webrtc.html#configure-mobile-apps") } + modifier = Modifier.clickable { uriHandler.openExternalLink("https://simplex.chat/docs/webrtc.html#configure-mobile-apps") } ) { Text(stringResource(MR.strings.how_to), color = MaterialTheme.colors.primary) Icon( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index 7ea656e1e4..c826b1dc51 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -75,7 +75,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: ( } val simplexTeamUri = - "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" + "simplex:/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw?h=smp6.simplex.im" @Composable fun SettingsLayout( @@ -207,7 +207,7 @@ fun ChatLockItem( } @Composable private fun ContributeItem(uriHandler: UriHandler) { - SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat#contribute") }) { + SectionItemView({ uriHandler.openExternalLink("https://github.com/simplex-chat/simplex-chat#contribute") }) { Icon( painterResource(MR.images.ic_keyboard), contentDescription = "GitHub", @@ -235,7 +235,7 @@ fun ChatLockItem( } @Composable private fun StarOnGithubItem(uriHandler: UriHandler) { - SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) { + SectionItemView({ uriHandler.openExternalLink("https://github.com/simplex-chat/simplex-chat") }) { Icon( painter = painterResource(MR.images.ic_github), contentDescription = "GitHub", @@ -268,7 +268,7 @@ fun ChatLockItem( } @Composable fun InstallTerminalAppItem(uriHandler: UriHandler) { - SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) { + SectionItemView({ uriHandler.openExternalLink("https://github.com/simplex-chat/simplex-chat") }) { Icon( painter = painterResource(MR.images.ic_github), contentDescription = "GitHub", diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index 3b6cf34b7c..e5c731f3b2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -7,7 +7,9 @@ import SectionTextFooter import SectionView import SectionViewWithButton import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* +import androidx.compose.ui.layout.ContentScale import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* @@ -28,6 +30,7 @@ import chat.simplex.common.model.MsgContent import chat.simplex.common.platform.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.newchat.* +import chat.simplex.common.BuildConfigCommon import chat.simplex.res.MR @Composable @@ -35,6 +38,7 @@ fun UserAddressView( chatModel: ChatModel, shareViaProfile: Boolean = false, autoCreateAddress: Boolean = false, + onboarding: Boolean = false, close: () -> Unit ) { // TODO close when remote host changes @@ -75,17 +79,31 @@ fun UserAddressView( addressSettings = AddressSettings(businessAddress = false, autoAccept = null, autoReply = null) ) - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.share_address_with_contacts_question), - text = generalGetString(MR.strings.add_address_to_your_profile), - confirmText = generalGetString(MR.strings.share_verb), - onConfirm = { - setProfileAddress(true) - shareViaProfile.value = true - } - ) + val hasRelevantContacts = chatModel.chats.value.any { chat -> + val ci = chat.chatInfo + ci is ChatInfo.Direct && + ci.contact.active && + !ci.contact.isContactCard && + !ci.contact.contactConnIncognito + } + if (hasRelevantContacts) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.share_address_with_contacts_question), + text = generalGetString(MR.strings.add_address_to_your_profile), + confirmText = generalGetString(MR.strings.share_verb), + onConfirm = { + setProfileAddress(true) + shareViaProfile.value = true + } + ) + progressIndicator.value = false + } else { + setProfileAddress(true) + shareViaProfile.value = true + } + } else { + progressIndicator.value = false } - progressIndicator.value = false } } @@ -103,6 +121,7 @@ fun UserAddressView( user = user.value, userAddress = userAddress.value, shareViaProfile, + onboarding = onboarding, createAddress = ::createAddress, showAddShortLinkAlert = { shareAddress: (() -> Unit)? -> showAddShortLinkAlert(progressIndicator = progressIndicator, share = ::share, shareAddress = shareAddress) @@ -249,6 +268,7 @@ private fun UserAddressLayout( user: User?, userAddress: UserContactLinkRec?, shareViaProfile: MutableState, + onboarding: Boolean = false, createAddress: () -> Unit, showAddShortLinkAlert: ((() -> Unit)?) -> Unit, learnMore: () -> Unit, @@ -259,68 +279,100 @@ private fun UserAddressLayout( saveAddressSettings: (AddressSettingsState, MutableState) -> Unit, ) { ColumnWithScrollBar { - AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId)) + if (!onboarding) { + AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId)) + } + if (BuildConfigCommon.SIMPLEX_ASSETS && userAddress != null) { + Image( + painterResource(if (isInDarkTheme()) { + if (onboarding) MR.images.simplex_address_light else MR.images.simplex_address_small_light + } else { + if (onboarding) MR.images.simplex_address else MR.images.simplex_address_small + }), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxWidth() + ) + } Column( Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly ) { if (userAddress == null) { - SectionView(generalGetString(MR.strings.for_social_media).uppercase()) { - CreateAddressButton(createAddress) - } + if (!onboarding) { + SectionView(generalGetString(MR.strings.for_social_media).uppercase()) { + CreateAddressButton(createAddress) + } - SectionDividerSpaced() - SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { - CreateOneTimeLinkButton() - } + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { + CreateOneTimeLinkButton() + } - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) - SectionView { - LearnMoreButton(learnMore) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionView { + LearnMoreButton(learnMore) + } } } else { - val addressSettingsState = remember { mutableStateOf(AddressSettingsState(settings = userAddress.addressSettings)) } - val savedAddressSettingsState = remember { mutableStateOf(addressSettingsState.value) } val showShortLink = remember { mutableStateOf(true) } - SectionViewWithButton( - stringResource(MR.strings.for_social_media).uppercase(), - titleButton = if (userAddress.connLinkContact.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null - ) { + if (onboarding) { + Text( + stringResource(MR.strings.onboarding_post_address), + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), + style = MaterialTheme.typography.body1 + ) + LinkTextView(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value), true) + Text( + stringResource(MR.strings.onboarding_or_use_qr_code), + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), + style = MaterialTheme.typography.body1 + ) SimpleXCreatedLinkQRCode(userAddress.connLinkContact, short = showShortLink.value) - if (userAddress.shouldBeUpgraded) { - AddShortLinkButton(text = stringResource(MR.strings.add_short_link)) { showAddShortLinkAlert(null) } - } - ShareAddressButton { + } else { + val addressSettingsState = remember { mutableStateOf(AddressSettingsState(settings = userAddress.addressSettings)) } + val savedAddressSettingsState = remember { mutableStateOf(addressSettingsState.value) } + + SectionViewWithButton( + stringResource(MR.strings.for_social_media).uppercase(), + titleButton = if (userAddress.connLinkContact.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null + ) { + SimpleXCreatedLinkQRCode(userAddress.connLinkContact, short = showShortLink.value) if (userAddress.shouldBeUpgraded) { - showAddShortLinkAlert { share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) } - } else { - share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) + AddShortLinkButton(text = stringResource(MR.strings.add_short_link)) { showAddShortLinkAlert(null) } + } + ShareAddressButton { + if (userAddress.shouldBeUpgraded) { + showAddShortLinkAlert { share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) } + } else { + share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) + } + } + // ShareViaEmailButton { sendEmail(userAddress) } + BusinessAddressToggle(addressSettingsState) { saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) } + AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAddressSettings) + + if (addressSettingsState.value.businessAddress) { + SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations)) } } - // ShareViaEmailButton { sendEmail(userAddress) } - BusinessAddressToggle(addressSettingsState) { saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) } - AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAddressSettings) - if (addressSettingsState.value.businessAddress) { - SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations)) + SectionDividerSpaced(maxTopPadding = addressSettingsState.value.businessAddress) + SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { + CreateOneTimeLinkButton() + } + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + LearnMoreButton(learnMore) } - } - SectionDividerSpaced(maxTopPadding = addressSettingsState.value.businessAddress) - SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { - CreateOneTimeLinkButton() - } - SectionDividerSpaced(maxBottomPadding = false) - SectionView { - LearnMoreButton(learnMore) - } - - SectionDividerSpaced(maxBottomPadding = false) - SectionView { - DeleteAddressButton(deleteAddress) - SectionTextFooter(stringResource(MR.strings.your_contacts_will_remain_connected)) + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + DeleteAddressButton(deleteAddress) + SectionTextFooter(stringResource(MR.strings.your_contacts_will_remain_connected)) + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt index bbd2a0af49..a62a58cb10 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt @@ -769,7 +769,7 @@ fun UsageConditionsView( .clip(shape = CircleShape) .clickable { val commitUrl = "https://github.com/simplex-chat/simplex-chat/commit/$commit" - uriHandler.openUriCatching(commitUrl) + uriHandler.openExternalLink(commitUrl) } .padding(horizontal = 6.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, @@ -826,13 +826,22 @@ fun UsageConditionsView( @Composable fun SimpleConditionsView( - rhId: Long? + rhId: Long?, + onAccept: () -> Unit ) { ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), enableAlphaChanges = false, withPadding = false, bottomPadding = DEFAULT_PADDING) Column(modifier = Modifier.weight(1f).padding(bottom = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF)) { ConditionsTextView(rhId) } + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).padding(bottom = DEFAULT_PADDING * 2).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingActionButton( + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), + labelId = MR.strings.onboarding_conditions_accept, + onboarding = null, + onclick = onAccept + ) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index 1449e0cd0d..9e11b9a932 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -500,7 +500,7 @@ fun OperatorInfoView(serverOperator: ServerOperator) { Text(d) } val website = serverOperator.info.website - Text(website, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(website) }) + Text(website, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openExternalLink(website) }) } } } @@ -511,7 +511,7 @@ fun OperatorInfoView(serverOperator: ServerOperator) { SectionView { SectionItemView { val (text, link) = selfhost - Text(text, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(link) }) + Text(text, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openExternalLink(link) }) } } } @@ -787,7 +787,7 @@ private fun ConditionsLinkView(conditionsLink: String) { SectionItemView { val uriHandler = LocalUriHandler.current Text(stringResource(MR.strings.operator_conditions_failed_to_load), color = MaterialTheme.colors.onBackground) - Text(conditionsLink, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(conditionsLink) }) + Text(conditionsLink, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openExternalLink(conditionsLink) }) } } @@ -821,13 +821,13 @@ fun ConditionsLinkButton() { val commit = chatModel.conditions.value.currentConditions.conditionsCommit ItemAction(stringResource(MR.strings.operator_open_conditions), painterResource(MR.images.ic_draft), onClick = { val mdUrl = "https://github.com/simplex-chat/simplex-chat/blob/$commit/PRIVACY.md" - uriHandler.openUriCatching(mdUrl) showMenu.value = false + uriHandler.openExternalLink(mdUrl) }) ItemAction(stringResource(MR.strings.operator_open_changes), painterResource(MR.images.ic_more_horiz), onClick = { val commitUrl = "https://github.com/simplex-chat/simplex-chat/commit/$commit" - uriHandler.openUriCatching(commitUrl) showMenu.value = false + uriHandler.openExternalLink(commitUrl) }) } IconButton({ showMenu.value = true }) { @@ -838,11 +838,7 @@ fun ConditionsLinkButton() { private fun internalUriHandler(parentUriHandler: UriHandler): UriHandler = object: UriHandler { override fun openUri(uri: String) { - if (uri.startsWith("https://simplex.chat/contact#")) { - openVerifiedSimplexUri(uri) - } else { - parentUriHandler.openUriCatching(uri) - } + parentUriHandler.openExternalLink(uri) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt index b232c7994e..3be2456b72 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt @@ -335,7 +335,7 @@ private fun HowToButton() { SettingsActionItem( painterResource(MR.images.ic_open_in_new), stringResource(MR.strings.how_to_use_your_servers), - { uriHandler.openUriCatching("https://simplex.chat/docs/server.html") }, + { uriHandler.openExternalLink("https://simplex.chat/docs/server.html") }, textColor = MaterialTheme.colors.primary, iconColor = MaterialTheme.colors.primary ) 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 2a76fa292a..95ec53287a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -26,7 +26,7 @@ خوادم الاتصالات الجديدة لملف تعريف الدردشة الحالي الخاص بك سيتم تغيير عنوان الاستلام إلى خادم مختلف. سيتم إكمال تغيير العنوان بعد اتصال المرسل بالإنترنت. هذا الرابط ليس رابط اتصال صالح! - يسمح + اسمح أضِف خوادم مُعدة مسبقًا أضِف إلى جهاز آخر سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا! @@ -106,7 +106,7 @@ تجزئة رسالة سيئة معرّف رسالة سيئ انتهت المكالمة - تغير + غيِّر لون إضافي ثانوي " \nمتوفر في v5.1" @@ -122,7 +122,7 @@ 1 دقيقة 30 ثانية ألغِ الرسالة الحيّة - إلغاء + ألغِ لكل جهة اتصال وعضو في المجموعة\n. الرجاء ملاحظة: إذا كان لديك العديد من الاتصالات، فقد يكون استهلاك البطارية وحركة المرور أعلى بكثير وقد تفشل بعض الاتصالات.]]> جارٍ الاتصال… مكالمة صوتية @@ -162,19 +162,19 @@ مفعّل مفعّلة لك يمكن لجهات الاتصال تحديد الرسائل لحذفها؛ ستتمكن من مشاهدتها. - جار الاتصال… + يتصل… خطأ في الإتصال (المصادقة) خطأ في حذف جهة الاتصال جهة الاتصال مخفية: - نسخ + انسخ اتصل متصل انضمام إلى المجموعة؟ اتصل عبر رابط لمرة واحدة؟ تغيير عنوان الاستلام؟ نٌسخت إلى الحافظة - مسح - امسح الدردشة + امحُ + امحُ الدردشة أنشئ عنوان الدردشات تأكيد عبارة المرور الجديدة… @@ -199,19 +199,19 @@ تأكد من بيانات اعتمادك أنشئ عنوان SimpleX متابعة - تحدث مع المطورين + دردش مع المطوِّرين سياق الأيقونة إحباط تغيير العنوان؟ إحباط سيتم إحباط تغيير العنوان. سيتم استخدام عنوان الاستلام القديم. - مسح الدردشة؟ + محو الدردشة؟ وحدة تحكم الدردشة ضبط خوادم ICE الاتصال ملف تعريف الدردشة الإصدار الأساسي: v%s أنشئ ملف تعريف - جار الاتصال… + يتصل… انتهى متصل %1$d تخطت الرسائل @@ -221,13 +221,13 @@ خطأ في تعمية قاعدة البيانات توقفت الدردشة مكتمل - جاري الاتصال (أعلن) + يتصل (أُعلن) الاتصال إحباط تغيير العنوان أنشئ مجموعة سرية قارن رموز الأمان مع جهات اتصالك. الواجهة الصينية والاسبانية - مسح + امحُ %1$s يريد التواصل معك عبر جارِ تغيير العنوان… جارِ تغيير العنوان ل%s… @@ -240,17 +240,17 @@ تغيير وضع التدمير الذاتي تغيير رمز المرور التدمير الذاتي تأكيد ترقيات قاعدة البيانات - الاتصال (دعوة مقدمة) - مسح + يتصل (دعوة مقدمة) + امحُ خطأ في إنشاء رابط المجموعة (حاضِر) فعّل أبقِ TCP على قيد الحياة - جار الاتصال… - جار الاتصال… + يتصل… + يتصل… أرسلت طلب الاتصال! حُذفت قاعدة بيانات الدردشة جارِ تغيير العنوان… - جار الاتصال (قُبِل) + يتصل (قُبِل) فُحصت جهة الاتصال %1$s أعضاء أنشئ رابط المجموعة @@ -271,7 +271,7 @@ اتصل عبر عنوان التواصل؟ خطأ في حذف رابط المجموعة التحقق من الرسائل الجديدة كل 10 دقائق لمدة تصل إلى دقيقة واحدة - جار الاتصال + يتصل خطأ خطأ في حذف ملف تعريف المستخدم زر الاغلاق @@ -279,7 +279,7 @@ تغيير الدور أدخل كلمة المرور في البحث تسمح جهة الاتصال - تأكيد + أكِّد جهة الاتصال ليست متصلة بعد! اتصال تواصل عبر الرابط @@ -294,7 +294,7 @@ خطأ في حذف اتصال جهة الاتصال المنتظر أدخل رسالة ترحيب… متصل - جار الاتصال + يتصل مفعّلة للاتصال تغيير رمز المرور متصل @@ -308,18 +308,18 @@ تواصل عبر الرابط / رمز QR أنشئ رابط دعوة لمرة واحدة تحقق من عنوان الخادم وحاول مرة أخرى. - امسح التحقُّق + امحُ التحقُّق أنشئ عنوانًا للسماح للأشخاص بالتواصل معك. أدخل الخادم يدويًا ملون لدى جهة الاتصال التعمية بين الطريفين أنشئ أنشئ ملف تعريفك - مكالمة جارية... + مكالمة جارية فعّل التدمير الذاتي الموافقة على التعمية… الموافقة على التعمية لـ%s… - متصل (مقدم) + يتصل (مقدم) وافق التعمية التعمية نعم التعمية نعم ل%s @@ -335,7 +335,7 @@ احذف جميع الملفات احذف بعد احذف الملف - حذف + احذف حذف رسالة العضو؟ احذف احذف الرسائل @@ -398,7 +398,7 @@ مخصص تخصيص ومشاركة سمات الألوان. الخروج بدون حفظ - أدوات المطور + أدوات المطوِّر احذف قائمة الانتظار خطأ في تحديث خصوصية المستخدم مسح رمز QR.]]> @@ -471,9 +471,8 @@ يمكن للأعضاء إرسال الملفات والوسائط. تفضيلات المجموعة سريع ولا تنتظر حتى يصبح المرسل متصلاً بالإنترنت! - إخفاء + أخفِ كيفية الاستخدام - كيف يعمل SimpleX التخفي عبر رابط عنوان جهة الاتصال رمز الأمان غير صحيحة! الإشعارات الفورية مُعطَّلة @@ -602,7 +601,7 @@ مصادقة الجهاز غير مفعّلة. يمكنك تشغيل قفل SimpleX عبر الإعدادات، بمجرد تفعيل مصادقة الجهاز. نزّل الملف عطّل قفل SimpleX - تحرير + حرّر اسم ملف التعريف: البريد الإلكتروني أدخل أسمك: @@ -709,7 +708,7 @@ خطأ في تسليم الرسالة الشبكة والخوادم إشراف - فتح في تطبيق الجوال، ثم انقر فوق اتصال في التطبيق.]]> + فتح في تطبيق الجوال، ثم انقر فوق اتصال في التطبيق.]]> تحت الإشراف في: %s ردود الفعل الرسائل ممنوعة في هذه الدردشة. أُشرف بواسطة %s @@ -798,7 +797,7 @@ جارِ فتح قاعدة البيانات… جهة اتصالك فقط يمكنها إرسال رسائل صوتية. ألصق - لم يُعثر على عبارة المرور في Keystore، يُرجى إدخالها يدويًا. ربما حدث هذا إذا استعدت بيانات التطبيق باستخدام أداة النسخ الاحتياطي. إذا لم يكن الأمر كذلك، يُرجى التواصل مع المطورين. + لم يُعثر على عبارة المرور في Keystore، يُرجى إدخالها يدويًا. ربما حدث هذا إذا استعدت بيانات التطبيق باستخدام أداة النسخ الاحتياطي. إذا لم يكن الأمر كذلك، يُرجى التواصل مع المطوِّرين. افتح الدردشة قد يؤدي فتح الرابط في المتصفح إلى تقليل خصوصية الاتصال وأمانه. ستظهر روابط SimpleX غير الموثوقة باللون الأحمر. أنت فقط يمكنك إضافة ردود الفعل على الرسالة. @@ -816,7 +815,7 @@ مكالمة قيد الانتظار تقوم أجهزة العميل فقط بتخزين ملفات تعريف المستخدمين وجهات الاتصال والمجموعات والرسائل. صفّر الألوان - حفظ + احفظ عنوان الخادم المُعد مسبقًا حفظ وإشعار أعضاء المجموعة دوري @@ -827,18 +826,18 @@ صورة ملف التعريف الإشعارات خاصة يُرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها. - يُرجى تحديث التطبيق والتواصل مع المطورين. + يُرجى تحديث التطبيق والتواصل مع المطوِّرين. دليل المستخدم.]]> غيّر ملفات تعريف الدردشة اسحب الوصول - كشف + اكشف سيتم إيقاف استلام الملف. رفض قيم التطبيق منفذ احفظ إعدادات عنوان SimpleX إعادة تعريف الخصوصية - الرجاء الإبلاغ للمطورين. + يُرجى إبلاغ المطوِّرين بذلك. الخصوصية والأمان أزل إزالة عبارة المرور من Keystore؟ @@ -853,7 +852,7 @@ استلمت إجابة… مستودع GitHub.]]> رفض - يحمي خادم الترحيل عنوان IP الخاص بك، ولكن يمكنه مراقبة مدة المكالمة. + يحمي خادم المُرحل عنوان IP الخاص بك، ولكن يمكنه مراقبة مُدّة المكالمة. الرجاء إدخال كلمة المرور السابقة بعد استعادة نسخة احتياطية لقاعدة البيانات. لا يمكن التراجع عن هذا الإجراء. استعادة النسخة الاحتياطية لقاعدة البيانات؟ حفظ @@ -924,7 +923,7 @@ صفّر المنفذ %d خادم مُعد مسبقًا - يتم استخدام خادم الترحيل فقط إذا لزم الأمر. يمكن لطرف آخر مراقبة عنوان IP الخاص بك. + يُستخدم خادم المُرحل فقط إذا لزم الأمر. يمكن لطرف آخر مراقبة عنوان IP الخاص بك. حفظ وإشعار جهة الاتصال إعادة التشغيل استلمت في: %s @@ -946,7 +945,7 @@ إرسال رسالة إرسال أرسل رسالة حيّة - فشلت تجربة الخادم! + فشل اختبار الخادم! احفظ عبارة المرور في Keystore أرسل رسالة مباشرة إرسال عبر @@ -960,8 +959,8 @@ رسالة مرسلة عيّن تفضيلات المجموعة عيّنها بدلاً من استيثاق النظام. - مشاركة - إرسال + شارك + أرسل احفظ عبارة المرور وافتح الدردشة حدد جهات الاتصال تعيين يوم واحد @@ -1048,7 +1047,7 @@ يبدأ… شُغّل قفل SimpleX أظهر: - أظهر خيارات المطور + أظهر خيارات المطوِّر simplexmq: v%s (%2s) يتطلب الخادم إذنًا لإنشاء قوائم انتظار، تحقق من كلمة المرور. يتطلب الخادم إذنًا للرفع، تحقق من كلمة المرور. @@ -1093,7 +1092,7 @@ اختبر الخوادم لا معرّفات مُستخدم دعم SIMPLEX CHAT - تبديل + بدِّل العنوان الرئيسي سيتم وضع علامة على الرسالة على أنها تحت الإشراف لجميع الأعضاء. انقر للانضمام @@ -1117,7 +1116,7 @@ لاستلام الإشعارات، يُرجى إدخال عبارة مرور قاعدة البيانات مصادقة النظام يعمل التعمية واتفاقية التعمية الجديدة غير مطلوبة. قد ينتج عن ذلك أخطاء في الاتصال! - لا يمكن فك ترميز الصورة. من فضلك، جرب صورة مختلفة أو تواصل مع المطورين. + لا يمكن فك ترميز الصورة. من فضلك، جرّب صورة مختلفة أو تواصل مع المطوِّرين. سيتم حذف الرسالة لجميع الأعضاء. الصور كثيرة! مقاطع الفيديو كثيرة! @@ -1253,13 +1252,13 @@ إلغاء إخفاء ملف تعريف يجب أن تكون جهة الاتصال متصلة بالإنترنت حتى يكتمل الاتصال. \nيمكنك إلغاء هذا الاتصال وإزالة جهة الاتصال (والمحاولة لاحقًا باستخدام رابط جديد). - فتح في تطبيق الجوال.]]> + فتح في تطبيق الجوّال.]]> استخدم للاتصالات الجديدة استخدم الخادم عنوان خادمك قاعدة بيانات دردشتك أنت مدعو إلى المجموعة. انضم للتواصل مع أعضاء المجموعة. - لقد انضممت إلى هذه المجموعة. جارِ الاتصال بدعوة عضو المجموعة. + لقد انضممت إلى هذه المجموعة. يتصل بدعوة عضو المجموعة. غيّرتَ العنوان ل%s إلغاء إخفاء ملف تعريف الدردشة الرسائل الصوتية ممنوعة في هذه الدردشة. @@ -1284,7 +1283,7 @@ مكالمة فيديو الرسائل الصوتية ممنوعة. فتح القفل - رفع الملف + ارفع الملف لا يمكن التحقق منك؛ الرجاء المحاولة مرة اخرى. رسالة صوتية رسالة صوتية… @@ -1292,14 +1291,14 @@ أنت المراقب! تحتاج إلى السماح لجهة اتصالك بإرسال رسائل صوتية لتتمكن من إرسالها. أرسلت جهة اتصالك ملفًا أكبر من الحجم الأقصى المعتمد حاليًا (%1$s). - الاتصال بمطوري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات.]]> + الاتصال بمطوِّري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات.]]> خادمك يُخزن ملف تعريفك على جهازك ومشاركته فقط مع جهات اتصالك. لا تستطيع خوادم SimpleX رؤية ملف تعريفك. الفيديو مقفل الفيديو مُشغَّل مع رسالة ترحيب اختيارية. قريباً! - هذه الميزة ليست مدعومة بعد. جرب الإصدار القادم. + هذه الميزة ليست مدعومة بعد. جرّب الإصدار القادم. عطّل (الاحتفاظ بتجاوزات المجموعة) فعِّل لجميع المجموعات إرسال الإيصالات مفعّلة لـ%d مجموعات @@ -1366,7 +1365,7 @@ فعّل وضع التخفي عند الاتصال. أرسل لاتصال طُلب اتصال - جارٍ الاتصال بالفعل! + يتصل بالفعل! مجموعات أفضل و%d أحداث أخرى جارٍ انضمام بالفعل إلى المجموعة! @@ -1388,7 +1387,7 @@ اتصل بنفسك؟ سطح المكتب متصل بسطح المكتب - جار الاتصال بسطح المكتب + الاتصال بسطح المكتب أجهزة سطح المكتب الاسم الصحيح لـ%s؟ حذف %d رسالة؟ @@ -1408,10 +1407,10 @@ مُكتشف عبر الشبكة المحلية قطع اتصال سطح المكتب؟ إصدار تطبيق سطح المكتب %s غير متوافق مع هذا التطبيق. - توسيع + وسِّع هل تريد تكرار طلب الاتصال؟ خطأ في إعادة التفاوض بشأن التعمية - %1$s.]]> + %1$s.]]> خطأ لقد انضممت بالفعل إلى المجموعة عبر هذا الرابط. %s و%s @@ -1429,7 +1428,7 @@ (جديد)]]> فك ربط سطح المكتب؟ خيارات سطح المكتب المرتبطة - لا يمكن فك تشفير الفيديو. من فضلك، جرب مقطع فيديو مختلفًا أو اتصل بالمطورين. + لا يمكن فك ترميز الفيديو. من فضلك، جرّب مقطع فيديو مختلفًا أو اتصل بالمطوِّرين. %s متصل أسطح المكتب المرتبطة مجموعات التخفي @@ -1466,7 +1465,7 @@ تحقق من الرمز على الجوّال أدخل اسم الجهاز هذا… خطأ - لقد شاركت مسار ملف غير صالح. أبلغ عن المشكلة لمطوري التطبيق. + لقد شاركت مسار ملف غير صالح. أبلغ عن المشكلة لمطوِّري التطبيق. اسم غير صالح! ألصق عنوان سطح المكتب %1$s!]]> @@ -1483,13 +1482,13 @@ تحقق من الاتصال أعِد التحميل عشوائي - في انتظار اتصال الجوال: - للسماح لتطبيق الجوال بالاتصال بسطح المكتب، افتح هذا المنفذ في جدار الحماية لديك، إذا فعلته + في انتظار اتصال الجوّال: + للسماح لتطبيق الجوّال بالاتصال بسطح المكتب، افتح هذا المنفذ في جدار الحماية لديك، إذا فعّلته أنشئ ملف تعريف الدردشة عرض التحطم فتح منفذ في جدار الحماية - اقطع اتصال الجوالات - لا يوجد جوال متصل + اقطع اتصال الجوّالات + لا يوجد جوّال متصل خطأ في إظهار المحتوى خطأ في إظهار الرسالة انتهت المكالمة %1$s @@ -1526,30 +1525,26 @@ أظهر الأخطاء الداخلية خطأ فادح خطأ داخلي - يُرجى إبلاغ المطورين بذلك: -\n%s - يُرجى إبلاغ المطورين بذلك: -\n%s -\n -\nيوصى بإعادة تشغيل التطبيق. + يُرجى إبلاغ المطوِّرين بذلك: \n%s + يُرجى إبلاغ المطوِّرين بذلك: \n%s \n \nيوصى بإعادة تشغيل التطبيق. أعد تشغيل الدردشة - %s غير نشط]]> + %s غير نشط]]> أظهر مكالمات API البطيئة غير معروف حدّثت ملف التعريف - %s مفقود]]> - %s لديه إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين]]> - %s في حالة سيئة]]> + %s مفقود]]> + %s لديه إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين]]> + %s في حالة سيئة]]> اسم العرض غير صالح! اسم العرض هذا غير صالح. الرجاء اختيار اسم آخر. توقف الاتصال توقف الاتصال - %s بسبب: %s]]> + %s بسبب: %s]]> قُطع الاتصال بسبب: %s - %s]]> - %s]]> + %s]]> + %s]]> سطح المكتب غير نشط - %s مشغول]]> + %s مشغول]]> انتهت المهلة أثناء الاتصال بسطح المكتب قُطع اتصال سطح المكتب الاتصال بسطح المكتب في حالة سيئة @@ -1558,7 +1553,7 @@ يحتوي سطح المكتب على إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين العضو %1$s وظيفة بطيئة - خيارات المطور + خيارات المطوِّر تغيّر العضو %1$s إلى %2$s أزلت عنوان الاتصال أزلت صورة ملف التعريف @@ -1581,7 +1576,7 @@ حدث خطأ أثناء إنشاء الرسالة حدث خطأ أثناء حذف الملاحظات الخاصة ملاحظات خاصة - مسح الملاحظات الخاصة؟ + محو الملاحظات الخاصة؟ أُنشئ في: %s رسالة محفوظة إلغاء حظر العضو للجميع؟ @@ -1604,7 +1599,7 @@ مكالمة فيديو مكالمة صوتية أنهيّ المكالمة - متصفح الويب الافتراضي مطلوب للمكالمات. يُرجى تضبيط المتصفح الافتراضي في النظام، ومشاركة المزيد من المعلومات مع المطورين. + متصفح الويب الافتراضي مطلوب للمكالمات. يُرجى تضبيط المتصفح الافتراضي في النظام، ومشاركة المزيد من المعلومات مع المطوِّرين. حدث خطأ أثناء فتح المتصفح أرشف وأرفع يمكن للمُدراء حظر عضو للجميع. @@ -1648,7 +1643,7 @@ تحقق من عبارة المرور تأكد من أنك تتذكر عبارة مرور قاعدة البيانات لترحيلها. التحقق من عبارة مرور قاعدة البيانات - خطأ في عرض الإشعار، تواصل بالمطورين. + خطأ في عرض الإشعار، تواصل بالمطوِّرين. مكالمات صورة في صورة استخدم التطبيق أثناء المكالمة. رحّل إلى جهاز آخر عبر رمز QR. @@ -1819,8 +1814,7 @@ معلومات قائمة انتظار الرسائل احمِ عنوان IP الخاص بك من مُرحلات المُراسلة التي اختارتها جهات اتصالك. \nفعّل في إعدادات *الشبكة والخوادم*. سمات دردشة جديدة - حدث خطأ أثناء تهيئة WebView. حدّث نظامك إلى الإصدار الجديد. يُرجى التواصل بالمطورين. -\nError: %s + حدث خطأ أثناء تهيئة WebView. حدّث نظامك إلى الإصدار الجديد. يُرجى التواصل بالمطوِّرين. \nError: %s تحسين تسليم الرسائل مع انخفاض استخدام البطارية. مفتاح خاطئ أو عنوان مجموعة الملف غير معروف - على الأرجح حُذف الملف. @@ -1834,8 +1828,7 @@ حالة الرسالة: %s خطأ في النسخ استُخدم هذا الرابط مع جوّال آخر، يُرجى إنشاء رابط جديد على سطح المكتب. - يُرجى التحقق من اتصال الهاتف المحمول وسطح المكتب بنفس الشبكة المحلية، وأن جدار حماية سطح المكتب يسمح بالاتصال. -\nيُرجى مشاركة أي مشاكل أُخرى مع المطورين. + يُرجى التحقق من اتصال الهاتف المحمول وسطح المكتب بنفس الشبكة المحلية، وأن جدار حماية سطح المكتب يسمح بالاتصال. \nيُرجى مشاركة أي مشاكل أُخرى مع المطوِّرين. لا يمكن إرسال الرسالة تفضيلات الدردشة المحدّدة تحظر هذه الرسالة. التفاصيل @@ -1919,7 +1912,7 @@ رُفع القطع متصل الخوادم المتصلة - جارِ الاتصال + يتصل الاتصالات النشطة ملف التعريف الحالي أخطاء الحذف @@ -1988,7 +1981,7 @@ المكالمات ممنوعة! لا يمكن مكالمة أحد أعضاء المجموعة لا يمكن إرسال رسالة إلى عضو المجموعة - جارِ الاتصال بجهة الاتصال، يُرجى الانتظار أو التحقق لاحقًا! + يتصل بجهة الاتصال، يُرجى الانتظار أو التحقق لاحقًا! جهات الاتصال المؤرشفة ادعُ لا توجد جهات اتصال مُصفاة @@ -1998,7 +1991,7 @@ يُرجى الطلب من جهة اتصالك تفعيل المكالمات. حذف %d رسائل الأعضاء؟ سيتم وضع علامة على الرسائل للحذف. سيتمكن المُستلم/(المُستلمون) من الكشف عن هذه الرسائل. - حدد + حدّد سيتم حذف الرسائل لجميع الأعضاء. سيتم وضع علامة على الرسائل على أنها تحت الإشراف لجميع الأعضاء. الرسالة @@ -2030,7 +2023,6 @@ ادعُ خيارات الوسائط الجديدة شغّل من قائمة الدردشة. - تبديل قائمة الدردشة: يمكنك تغييره في إعدادات المظهر. نزّل الإصدارات الجديدة من GitHub. ترقية التطبيق تلقائيًا @@ -2211,7 +2203,7 @@ الإشعارات والبطارية فقط مالكي الدردشة يمكنهم تغيير التفضيلات. الخصوصية لعملائك. - الجوالات عن بُعد + الجوّالات عن بُعد ادعُ للدردشة مغادرة المجموعة؟ سيتم إزالة العضو من الدردشة - لا يمكن التراجع عن هذا! @@ -2352,12 +2344,11 @@ إلغاء حظر الأعضاء للجميع؟ حظر الأعضاء للجميع؟ سيتم عرض رسائل من هؤلاء الأعضاء! - لا يمكن قراءة عبارة المرور في Keystore، يُرجى إدخالها يدويًا. قد يكون هذا قد حدث بعد تحديث النظام غير متوافق مع التطبيق. إذا لم يكن الأمر كذلك، فيُرجى التواصل مع المطورين. + لا يمكن قراءة عبارة المرور في Keystore، يُرجى إدخالها يدويًا. قد يكون هذا قد حدث بعد تحديث النظام غير متوافق مع التطبيق. إذا لم يكن الأمر كذلك، فيُرجى التواصل مع المطوِّرين. سيتم إزالة الأعضاء من المجموعة - لا يمكن التراجع عن هذا! المشرفين - لا يمكن قراءة عبارة المرور في Keystore. قد يكون هذا قد حدث بعد تحديث النظام غير متوافق مع التطبيق. إذا لم يكن الأمر كذلك، فيُرجى التواصل مع المطورين. + لا يمكن قراءة عبارة المرور في Keystore. قد يكون هذا قد حدث بعد تحديث النظام غير متوافق مع التطبيق. إذا لم يكن الأمر كذلك، فيُرجى التواصل مع المطوِّرين. موافقة الانتظار - ضبّط مُشغلي الخادم سياسة الخصوصية وشروط الاستخدام. لا يمكن الوصول إلى الدردشات الخاصة والمجموعات وجهات اتصالك لمشغلي الخادم. باستخدام SimpleX Chat، توافق على:\n- إرسال المحتوى القانوني فقط في المجموعات العامة.\n- احترام المستخدمين الآخرين – لا سبام. @@ -2515,7 +2506,7 @@ افتح الرابط النظيف افتح الرابط الكامل أزل تتبع الروابط - رابط مُرحل SimpleX + عنوان مُرحل SimpleX خطأ في وضع علامة \"مقروءة\" البصمة في عنوان الخادم الوجهة لا تتطابق مع الشهادة: %1$s. البصمة في عنوان خادم التحويل لا تتطابق مع الشهادة: %1$s. @@ -2541,4 +2532,126 @@ ابحث عن رسائل صوتية فيديوهات رسائل صوتية + إذا انضممت إلى قنوات أو أنشأتها، فستتوقف عن العمل نهائيًا. + %1$d/%2$d مُرحلات نشطة + %1$d/%2$d مُرحلات نشطة، %3$d فشلت + %1$d/%2$d مُرحلات متصلة + %1$d/%2$d مُرحلات متصلة، %3$d خطأ + %1$d مشترك + %1$d مشترك + وافقت + نشط + احظر المشترك للكل؟ + مُرحلات الدردشة + مُرحلات الدردشة + احذف القناة + احذف القناة؟ + حُذفت + حُذفت القناة + احذف المُرحل + إذاعة + إلغاء إنشاء القناة؟ + اختبر المُرحل لاسترداد اسمه.]]> + %1$s !]]> + قناة + قناة + قناة + اسم القناة بالكامل + رابط القناة + أعضاء القناة + اسم القناة + يُخزّن ملف تعريف القناة على أجهزة المشتركين وعلى مُرحلات الدردشة. + حُدِّث ملف تعريف القناة + ستُحذف القناة لجميع المشتركين - لا يمكن التراجع عن هذا الإجراء! + ستُحذف القناة من عِندك - لا يمكن التراجع عن هذا الإجراء! + ستبدأ القناة بالعمل مع %1$d من أصل %2$d من المُرحلات. أتود المتابعة؟ + مُرحل الدردشة + مُرحلات الدردشة + مُرحلات الدردشة توجّه الرسائل في القنوات التي تنشئها. + مُرحلات الدردشة توجّه الرسائل في القنوات في القناة. + تحقق من عنوان المُرحل وحاول مرة أخرى. + تحقق من اسم المُرحل وحاول مرة أخرى. + اضبط المُرحلات + اتصل + متصل + يتصل + أنشئ قناة عامة + أنشئ قناة عامة + أنشئ قناة عامة (تجريبي) + ينشئ قناة + %d أحداث القناة + فك ترميز الرابط + تم الإسقاط (%1$d محاولات) + حرّر ملف تعريف القناة + فعّل مُرحل دردشة واحد على الأقل لإنشاء قناة. + أدخل اسم المُرحل… + خطأ في إضافة المُرحل + خطأ في إنشاء القناة + خطأ في فتح القناة + خطأ: %s + خطأ في حفظ ملف تعريف القناة + فشل + فشل + احصل على الرابط + عنوان المُرحل غير صالح! + اسم المُرحل غير صالح! + مدعو + انضم للقناة + غادِر القناة + مغادرة القناة؟ + الرابط + رسالة خطأ + جديد + مُرحل دردشة جديد + لا مُرحلات دردشة + لا مُرحلات دردشة مفعّلة. + ليس كل المُرحلات متصلة + افتح قناة + افتح قناة جديدة + المالك + المالكون + عنوان المُرحل مسبق الضبط + اسم المُرحل مسبق الضبط + مُرحل + مُرحل + عنوان المُرحل + عنوان المُرحل + فشل اتصال المُرحل + رابط المُرحل + فشل اختبار المُرحل + أزِل المشترك + إزالة المشترك؟ + احفظ وأرسل إشعار للمشتركين في القناة + احفظ ملف تعريف القناة + يتطلب الخادم تفويضًا للاتصال بالمُرحل، يُرجى التحقق من كلمة المرور. + تحذير من الخادم + شارك عنوان المُرحل + مشترك + المشتركون + يستخدم المشتركون رابط المُرحل للاتصال بالقناة.\nاُستخدم عنوان المُرحل لإعداد هذا المُرحل للقناة. + ستُزيل المشترك من القناة - لا يمكن التراجع عن هذا الإجراء! + انقر انضم للقناة + فشل الاختبار عند الخطوة %s. + اختبر المُرحل + أُزيل التطبيق هذه الرسالة بعد %1$d محاولات لاستلامها. + عنوان مُرحل الدردشة هذا لا يمكن استخدامه للاتصال. + إلغاء حظر المشترك للجميع؟ + ملف القناة التعريفي حُدِّث + استخدم للقنوات الجديدة + استخدم مُرحل + تحقق + عبر %1$s + تسجيل الصوت غير مدعوم على منصتك + انتظر + رد الانتظار + أنت + أنت مشترك + بإمكانك مشاركة رابط أو رمز QR - وسيتمكن أي شخص من الانضمام إلى القناة. + لقد اتصلت بالقناة عبر رابط المُرحل هذا. + قناتك + قناتك + سيتم مشاركة ملف تعريفك %1$s مع مُرحلات القناة والمشتركين.\nيمكن للمُرحلات الوصول إلى رسائل القناة. + عنوان مُرحلك + اسم مُرحلك + ستتوقف عن تلقي الرسائل من هذه القناة، وسيتم الاحتفاظ بسجل الدردشة. 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 ac9f9b2fc8..34a091df31 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -74,6 +74,7 @@ end-to-end encryption.]]> end-to-end encryption with perfect forward secrecy, repudiation and break-in recovery.]]> quantum resistant e2e encryption with perfect forward secrecy, repudiation and break-in recovery.]]> + not end-to-end encrypted. Chat relays can see these messages.]]> This chat is protected by end-to-end encryption. This chat is protected by quantum resistant end-to-end encryption. @@ -191,6 +192,8 @@ Please check that you used the correct link or ask your contact to send you another one. Unsupported connection link This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + Channel temporarily unavailable + Channel has no active relays. Please try to join later. Connection error (AUTH) 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. Connection blocked @@ -463,6 +466,15 @@ Tap to start a new chat Chat with the developers You have no chats + Talk to someone + Let someone connect to you + Connect via link or QR code + Create your link + Invite someone privately + A link for one person to connect + Create your public address + Your public address + For anyone to reach you Loading chats… No filtered chats No chats in list %s. @@ -495,6 +507,7 @@ Favorites Contacts Groups + Channels Businesses Notes Reports @@ -531,8 +544,21 @@ Share file… Forward message… Forward messages… + Share channel… Cannot send message Selected chat preferences prohibit this message. + Share via chat + Tap to open + Channel link + Group link + Business address + Contact address + One-time link + (from owner) + (signed) + Error sharing channel + Link signature verified. + ⚠️ Signature verification failed: %s. Attach @@ -591,6 +617,7 @@ removed from group you left can\'t send messages + can\'t broadcast you are observer reviewed by admins member has old version @@ -887,7 +914,7 @@ New chat New message Add contact - Scan / Paste link + Paste link / Scan Paste link One-time invitation link 1-time link @@ -1040,7 +1067,7 @@ for each contact and group member.\nPlease note: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.]]> Update transport isolation mode? Use .onion hosts to No if SOCKS proxy does not support them.]]> - Please note: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]> + Please note: message and file relays are connected via SOCKS proxy. Calls use direct connection.]]> Private routing Always Unknown servers @@ -1115,12 +1142,12 @@ All your contacts will remain connected. All your contacts will remain connected. Profile update will be sent to your contacts. Share link - Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. Create an address to let people connect with you. Create SimpleX address - Share with contacts - Share address with contacts? - Profile update will be sent to your contacts. + Share with SimpleX contacts + Share address with SimpleX contacts? + Profile update will be sent to your SimpleX contacts. Stop sharing address? Stop sharing Auto-accept @@ -1137,6 +1164,11 @@ Or to share privately SimpleX address or 1-time link? Create 1-time link + New 1-time link + Send the link via any messenger - it\'s secure. Ask to paste into SimpleX. + Or show QR in person or via video call. + Use this address in your social media profile, website, or email signature. + Or use this QR - print or show online. Address settings Business address Add your team members to the conversations. @@ -1171,6 +1203,7 @@ Save and notify contact Save and notify contacts Save and notify group members + Save and notify channel subscribers Exit without saving @@ -1259,9 +1292,27 @@ Make a private connection Migrate from another device How it works + Be free\nin your network + Private and secure messaging. + The first network where you own\nyour contacts and groups. + Get started + Why SimpleX is built. + Your profile + On your phone, not on servers. + No account. No phone. No email. No ID.\nThe most secure encryption. + Enter profile name… + Migrate - - How SimpleX works + + You were born without an account. + Nobody tracked your conversations. No one drew a map of where you\'d been. Privacy was never a feature — it was the way of life. + Then we moved online, and every platform asked for a piece of you — your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way — telephone, email, messengers, social media. It seemed the only way possible. + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + Not a better lock on someone else\'s door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it — you are sovereign. + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + The oldest human freedom — to speak to another person without being watched — built on infrastructure that cannot betray it. + Because we destroyed the power to know who you are. So that your power can never be taken. + Be free in your network. To protect your privacy, SimpleX uses separate IDs for each of your contacts. Only client devices store user profiles, contacts, groups, and messages. end-to-end encrypted, with post-quantum security in direct messages.]]> @@ -1288,11 +1339,10 @@ Use random passphrase - Private chats, groups and your contacts are not accessible to server operators. - By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam. + Operators commit to:\n- Be independent\n- Minimize metadata usage\n- Run verified open-source code + You commit to:\n- Only legal content in public groups\n- Respect other users — no spam Privacy policy and conditions of use. Accept - Configure server operators Server operators Network operators SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. @@ -1309,6 +1359,15 @@ Update Continue + + Your network + Network routers cannot know\nwho talks to whom + Setup routers + Setup notifications + + + Network commitments + Incoming video call Incoming audio call @@ -1344,6 +1403,7 @@ Open SimpleX Chat to accept call Enable calls from lock screen via Settings. Open + Open external link? e2e encrypted @@ -1376,6 +1436,10 @@ bad message hash bad message ID duplicate message + dropped (%1$d attempts) + error: %s + Message error + The app removed this message after %1$d attempts to receive it. Skipped messages 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. Bad message hash @@ -1637,7 +1701,8 @@ Confirm database upgrades Reachable app toolbars Reachable chat toolbar - Toggle chat list: + Bottom bar + Top bar You can change it in Appearance settings. Show console in new window Show chat list in new window @@ -1706,7 +1771,9 @@ removed %1$s removed you deleted group + deleted channel updated group profile + updated channel profile invited via your group link requested connection New member wants to join the group. @@ -1717,6 +1784,7 @@ you removed %1$s you left group profile updated + channel profile updated you accepted this member Please wait for group moderators to review your request to join the group. @@ -1725,6 +1793,7 @@ %s, %s and %s connected %s, %s and %d other members connected %d group events + %d channel events and %d other events %s and %s %s, %s and %d members @@ -1818,6 +1887,7 @@ you: %1$s Delete group Delete channel + Cancel and delete channel Delete chat Delete group? Delete channel? @@ -1850,6 +1920,7 @@ Error creating member contact Error sending invitation Only group owners can change group preferences. + Only channel owners can change channel preferences. Only chat owners can change preferences. Address Share address @@ -1999,6 +2070,7 @@ Fully decentralized – visible only to members. Enter group name: Group full name: + Channel full name: Short description: Description too large Your chat profile will be sent to group members @@ -2007,8 +2079,11 @@ Group profile is stored on members\' devices, not on the servers. + Channel profile is stored on subscribers\' devices and on the chat relays. Save group profile + Save channel profile Error saving group profile + Error saving channel profile Preset servers @@ -2188,6 +2263,7 @@ Chat preferences Contact preferences Group preferences + Channel preferences Set group preferences Set member admission Your preferences @@ -2289,6 +2365,35 @@ History is not sent to new members. Members can report messsages to moderators. Reporting messages is prohibited in this group. + Chat with admins + Allow members to chat with admins. + Prohibit chats with admins. + Members can chat with admins. + Chats with admins are prohibited. + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + Enable chats with admins? + Enable + + + Subscriber reports + Allow sending direct messages to subscribers. + Prohibit sending direct messages to subscribers. + Send up to 100 last messages to new subscribers. + Do not send history to new subscribers. + Subscribers can send disappearing messages. + Subscribers can send direct messages. + Direct messages between subscribers are prohibited. + Subscribers can irreversibly delete sent messages. (24 hours) + Subscribers can add message reactions. + Subscribers can send voice messages. + Subscribers can send files and media. + Subscribers can send SimpleX links. + Subscribers can report messsages to moderators. + Up to 100 last messages are sent to new subscribers. + History is not sent to new subscribers. + Allow subscribers to chat with admins. + Subscribers can chat with admins. + Delete after %d sec %ds @@ -2325,6 +2430,7 @@ Chats with members No chats with members + Chats with members are disabled Delete chat Delete chat with member? @@ -2533,6 +2639,17 @@ Share your address 4 new interface languages Catalan, Indonesian, Romanian and Vietnamese - thanks to our users! + Public channels - speak freely 🚀 + Reliability: many relays per channel. + Ownership: you can run your own relays. + Security: owners hold channel keys. + Privacy: for owners and subscribers. + Easier to invite your friends 👋 + We made connecting simpler for new users. + Safe web links + - opt-in to send link previews.\n- use SOCKS proxy if enabled.\n- prevent hyperlink phishing.\n- remove link tracking. + Non-profit governance + To make SimpleX Network last. View updated conditions @@ -2868,17 +2985,32 @@ connecting deleted failed + removed by operator + removed new invited accepted active + inactive + All relays removed + All relays failed + No active relays + %1$d relays removed + %1$d relays failed + %1$d relays not active %1$d/%2$d relays active, %3$d failed + %1$d/%2$d relays active, %3$d removed + %1$d/%2$d relays active, %3$d errors %1$d/%2$d relays active %1$d/%2$d relays connected, %3$d errors + %1$d/%2$d relays connected, %3$d failed + %1$d/%2$d relays connected, %3$d removed %1$d/%2$d relays connected - %1$d relays + No relays + Add relays to restore message delivery. + Waiting for channel owner to add relays. RELAY @@ -2892,6 +3024,10 @@ Subscribers use relay link to connect to the channel.\nRelay address was used to set up this relay for the channel. You connected to the channel via this relay link. Remove subscriber + Remove relay + Remove relay? + Relay will be removed from channel - this cannot be undone! + This is the last active relay. Removing it will prevent message delivery to subscribers. Block subscriber for all? @@ -2901,17 +3037,29 @@ Channel name Creating channel Error creating channel + Relay results: + The connection reached the limit of undelivered messages + Network error + Error Cancel creating channel? - Cancel + Your new channel %1$s is connected to %2$d of %3$d relays.\nIf you cancel, the channel will be deleted - you can create it again. Enable at least one chat relay to create a channel. Your profile %1$s will be shared with channel relays and subscribers.\nRelays can access channel messages. Configure relays failed + Add + Add relay + Add relays + No available relays + Error adding relays + Relays added: %1$s. + Select relays + No relays selected + %d relay(s) selected Relay connection failed Not all relays connected Wait - Proceed - Channel will start working with %1$d of %2$d relays. Proceed? + Channel will start working with %1$d of %2$d relays. Continue? Relay address @@ -2924,4 +3072,11 @@ Unblock subscriber for all? + + + Enable link previews? + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + Link preview will be requested via SOCKS proxy. DNS lookup may still happen locally via your DNS resolver. + Enable + Disable \ 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 03a8bcfdc5..c691447b32 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -764,7 +764,6 @@ Информация Инсталирай SimpleX Chat за терминал Как работи - Как работи SimpleX Защитен от спам Игнорирай Покани членове @@ -1038,7 +1037,7 @@ SimpleX Лого SimpleX Екип SMP сървъри - Сподели адреса с контактите\? + Сподели адреса с контактите? Сподели линк Сподели с контактите Спри споделянето @@ -2040,7 +2039,6 @@ Пропусни тази версия Провери за актуализации БАЗА ДАННИ - Превключване на чат списъка: Можете да изпращате съобщения до %1$s от архивираните контакти. Достъпен панел Изпращането на съобщения на груповия член не е налично @@ -2328,7 +2326,6 @@ Чат с член Разговаряйте с членовете, преди да се присъединят. Нарушение на правилата на общността - Конфигуриране на сървърни оператори Свързване Свържете се по-бързо! 🚀 Връзката е блокирана diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml index 5c8f73bf93..7ab3f5a381 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml @@ -1216,7 +1216,6 @@ Sense identificadors d\'usuari. Per protegir la vostra privadesa SimpleX utilitza identificadors separats per a cadascun dels vostres contactes.. Obrir SimpleX - Com funciona SimpleX Només els dispositius client emmagatzemen perfils d\'usuari, contactes, grups i missatges. Notificacions privades Com afecta la bateria @@ -1975,7 +1974,6 @@ Barres d\'eines d\'aplicacions accessible Barres d\'eines de xat accessible Mostra la llista de xat en una finestra nova - Commuta la llista de xat: Mostrar consola en finestra nova Podeu canviar-la a la configuració de l\'aparença. Actualitzar i obrir el xat @@ -2342,7 +2340,6 @@ Els xats privats, els grups i els vostres contactes no són accessibles per als operadors de servidor. Acceptar En utilitzar SimpleX Chat accepteu:\n- enviar només contingut legal en grups públics.\n- Respectar els altres usuaris, sense correu brossa. - Configurar els operadors de servidor Enllaç al canal SimpleX Aquest enllaç requereix una versió de l\'aplicació més recent. Actualitzeu l\'aplicació o demaneu al vostre contacte que enviï un enllaç compatible. Enllaç de connexió no compatible 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 b559431261..df4907885c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -49,7 +49,7 @@ Vytvoření tajné skupiny Zadejte název skupiny: Úplný název skupiny: - Váš chat profil bude zaslán členům skupiny + Váš profil chatu bude zaslán členům skupiny Profil skupiny je uložen v zařízeních členů, nikoli na serverech. Uložit Aktualizovat nastavení sítě\? @@ -228,7 +228,7 @@ Použít proxy server SOCKS\? Použít přímé připojení k internetu\? Ne - Chat profil + Profil chatu Připojení simplexmq: v%s (%2s) Vytvořit adresu @@ -242,7 +242,6 @@ probíhající hovor Decentralizovaná Jak to funguje - Jak funguje SimpleX Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy. Soukromé oznámení Pravidelné @@ -276,9 +275,9 @@ Chyba při exportu databáze chatu Import Restartujte aplikaci, abyste mohli používat importovanou databázi chatu. - Smazat chat profil\? + Smazat profil chatu? Tuto akci nelze vzít zpět! Váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny. - Restartujte aplikaci a vytvořte nový chat profil. + Restartujte aplikaci a vytvořte nový profil chatu. Nejnovější verzi databáze chatu musíte používat POUZE v jednom zařízení, jinak se může stát, že přestanete přijímat zprávy od některých kontaktů. Soubory a média Smazat soubory a média\? @@ -315,7 +314,7 @@ změnila se vaše adresa Rozšířit výběr rolí Nelze pozvat kontakt! - Již máte chat profil se stejným názvem. Zvolte prosím jiné jméno. + Již máte profil chatu se stejným názvem. Zvolte prosím jiné jméno. Vytvořit frontu Zabezpečit frontu Okamžitá oznámení! @@ -372,7 +371,7 @@ Vaše nastavení Vaše SimpleX adresa Přístupová fráze k databázi a export - Vaše chat profily + Profily chatu Zaslat otázky a nápady Test serveru Servery ICE (jeden na řádek) @@ -462,7 +461,7 @@ Chyba při odebrání člena Chyba při ukládání profilu skupiny vteřiny - Umožňuje mít v jednom chat profilu mnoho anonymních spojení bez sdílení údajů mezi nimi. + Umožňuje mít v jednom profilu chatu mnoho anonymních spojení bez sdílení údajů mezi nimi. Pokud s někým sdílíte inkognito profil, bude použit pro skupiny, do kterých vás pozve. Systémové Hlasové zprávy @@ -484,7 +483,7 @@ Ověření zabezpečení připojení Francouzské rozhraní Díky uživatelům - překládejte prostřednictvím Weblate! - Více chat profilů + Více profilů chatu Různá jména, avataři a izolace přenosu. Návrh zprávy Zachování posledního návrhu zprávy s přílohami. @@ -615,7 +614,7 @@ Adresa vašeho serveru Použít server Použít pro nová připojení - Servery pro nová připojení vašeho aktuálního chat profilu. + Servery pro nová připojení v rámci vašeho aktuálního profilu chatu. Použít SimpleX Chat servery\? Použití SimpleX Chat serverů. Uložené servery WebRTC ICE budou odstraněny. @@ -629,7 +628,7 @@ Onion hostitelé budou použiti, pokud jsou k dispozici. Onion hostitelé nebudou použiti. Izolace přenosu - for each chat profile you have in the app.]]> + pro každý profil chatu, který máte v aplikaci.]]> pro všechny kontakty a členy skupin. \nUpozornění: Pokud máte mnoho připojení, může být spotřeba baterie a provoz podstatně vyšší a některá připojení mohou selhat.]]> Vzhled Verze aplikace @@ -726,7 +725,7 @@ Chyba při mazání databáze chatu Chyba při importu databáze chatu Databáze chatu odstraněna - Odstranit soubory všech chat profilů + Odstranit soubory všech profilů chatu Odstranit všechny soubory 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. Žádné přijaté ani odeslané soubory @@ -734,7 +733,7 @@ nikdy %s sekund(y) Zprávy - Toto nastavení se vztahuje na zprávy ve vašem aktuálním chat profilu. + Toto nastavení se vztahuje na zprávy ve vašem aktuálním profilu chatu. Smazat zprávy po Povolit automatické mazání zpráv\? Tuto akci nelze vzít zpět - zprávy odeslané a přijaté dříve, než bylo zvoleno, budou smazány. Může to trvat několik minut. @@ -862,7 +861,7 @@ Povolit TCP keep-alive Aktualizovat Smazat profil chatu? - Smazat chat profil pro + Smazat profil chatu pro Profil a připojení k serveru Pouze místní data profilu Režim inkognito chrání vaše soukromí používáním nového náhodného profilu pro každý kontakt. @@ -872,12 +871,12 @@ Kontakt povolil zaplé vyplé - Chat předvolby + Předvolby chatu Předvolby kontaktu Předvolby skupiny Přímé zprávy Mazání všem - zapnuty + zapnuto povoleno vám vypnuty Mizící zprávy zakázány. @@ -923,15 +922,14 @@ Italské rozhraní Díky uživatelům - překládejte prostřednictvím Weblate! Budete připojeni, jakmile bude zařízení vašeho kontaktu online, vyčkejte prosím nebo se podívejte později! - Váš chat profil bude odeslán -\nvašemu kontaktu + Váš profil chatu bude odeslán \nvašemu kontaktu Konverzace Sdílet jednorázovou pozvánku koncově šifrované moderované moderovaný %s Smazat zprávu člena\? - moderovaný + Moderovat Kontaktujte prosím správce skupiny. jste pozorovatel pozorovatel @@ -953,7 +951,7 @@ Chyba aktualizace soukromí uživatele Správa skupin Uvítací zpráva skupin - Skryté chat profily + Skryté profily chatu Hesla skrytých profilů Skrýt Skrýt profil @@ -964,7 +962,7 @@ \n- zakázat členy (role "pozorovatel") Uložit heslo profilu Ztlumit - Chraňte své chat profily heslem! + Chraňte své profily chatu pomocí hesla! Uložit a aktualizovat profil skupiny Ztlumit při neaktivitě! Heslo k zobrazení @@ -976,7 +974,7 @@ Uvítací zpráva Uvítací zpráva Zrušit ztlumení - Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce Chat profily. + Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce Profily chatu. Stále budete přijímat volání a upozornění od umlčených profilů pokud budou aktivní. Můžete skrýt nebo ztlumit uživatelský profil - Podržte pro menu. Odkrýt @@ -998,10 +996,10 @@ Zobrazit: ID databáze a možnost Izolace přenosu. Soubor bude přijat, jakmile váš kontakt dokončí nahrávání. - Smazat chat profil + Smazat profil chatu Smazat profil Heslo profilu - Odkrýt chat profil + Odkrýt profil chatu Odkrýt profil Žádost o přijetí videa Současně lze odeslat pouze 10 videí @@ -1104,7 +1102,7 @@ Heslo pro sebedestrukci změněno! Další zbarvení Sekundární - Vytvořit prázdný chat profil se zadaným názvem a otevřít aplikaci jako obvykle. + Vytvořit prázdný profil chatu se zadaným názvem a otevřít aplikaci jako obvykle. Pokud tento přístupový kód zadáte při otevření aplikace, všechna data budou nenávratně smazána! Další sekundární Pozadí @@ -1114,10 +1112,10 @@ Chyba načítání podrobností Info Hledat - Změnit chat profily + Změnit profily chatu Historie Přijatá zpráva - Poslaná zpráva + Odeslaná zpráva Mizící zpráva Poslat mizící zprávu 1 minutu @@ -1169,7 +1167,7 @@ BARVY MOTIVU Přizpůsobit motiv Aktualizace profilu bude zaslána vašim kontaktům. - Sdílet adresu s kontakty\? + Sdílet adresu s kontakty? Přestat sdílet adresu\? Vytvořit adresu, aby se s vámi lidé mohli spojit. Uložit nastavení SimpleX adresy @@ -1248,7 +1246,7 @@ šifrování povoleno pro %s vyžadováno opětovné vyjednávání šifrování pro %s Odesílání potvrzení o doručení je vypnuto pro %d kontakty. - Odesílání potvrzení o doručení bude povoleno pro všechny kontakty ve všech viditelných chat profilech. + Odesílání potvrzení o doručení bude povoleno pro všechny kontakty ve všech viditelných profilech chatu. Toto nastavení je pro váš aktuální profil opětovné vyjednávání šifrování povoleno opětovné vyjednávání šifrování povoleno pro %s @@ -1406,7 +1404,7 @@ Odpojit počítač? Vytvořit skupinu Kód relace - Vložit adres počítače + Vložit adresu počítače Blokovaný Lepší skupiny Rozbalit @@ -1471,7 +1469,7 @@ Blokovat člena Použít z PC Opakovat požadavek na připojení? - Vytvořit chat profil + Vytvořit profil chatu Zobrazit havarované Smazat a informovat kontakt Již se připojujete přes tento jednorázový odkaz! @@ -1544,7 +1542,7 @@ blokováno %s kontakt %1$s změnen na %2$s Vytvořeno v - Blok všem + Blokovat všem Blokovat člena všem? Chyba blokování člena všem Vylepšené doručovaní zpráv @@ -1744,7 +1742,7 @@ Kamera Kamera a mikrofon SimpleX odkazy jsou zakázány. - koncovým šifrováním s dokonalým dopředným utajením, odmítnutím a obnovením po vloupání.]]> + koncovým šifrováním s dokonalým dopředným utajením, odmítnutím a obnovením po vloupání.]]> Pokročilé nastavení Všechny barevné režimy Překročená kapacita - příjemci neobdrží dříve poslané zprávy. @@ -1902,7 +1900,7 @@ Chat databáze exportována Členu skupiny nelze odeslat zprávu Požádejte váš kontakt ať povolí volání. - Odeslaných odpovědí + Odeslaná odpověď Škálovat Přizpůsobit Rozmazání pro lepší soukromí. @@ -1946,7 +1944,7 @@ Smazat bez upozornění hledat Chyba přepínání profilu - Vyberte chat profil + Vyberte profil chatu zpráva otevřít Kontakt smazán! @@ -2055,7 +2053,7 @@ Žádné servery pro soukromé směrování chatů. Žádné servery pro příjem souborů. Žádné servery pro příjem zpráv. - Všechny chaty budou ze seznamu odebrány %s, a seznam bude smazán + Všechny chaty budou ze seznamu %s odebrány, a seznam bude smazán Pro sociální sítě Vzdálené telefony %s.]]> @@ -2241,14 +2239,13 @@ Můžete nastavit operátory v nastavení sítě a serverů. Ocas Zastavíte přijímání zpráv z tohoto chatu. Chat historie bude zachována. - Servery pro nové soubory vašeho aktuálního chat profilu + Servery pro nové soubory vašeho aktuálního profilu chatu Protokolu serveru se změnil. Operátor serveru se změnil. Zoom Nastavit výchozí téma Nahráno Ano - Přepnout chat seznam: Tuto akci nelze zrušit - zprávy odeslané a přijaté v tomto chatu dříve než vybraná, budou smazány. Statistiky serverů budou obnoveny - nemůže být vráceno! Odešlete soukromý report @@ -2266,7 +2263,7 @@ Použit pro zprávy Server přidán k operátoru %s. Průhlednost - Přepínání chat profilu pro 1-rázové pozvánky. + Přepínání profilu chatu pro jednorázové pozvánky. video Sdílet profil Reportování zpráv je zakázáno v této skupině. @@ -2288,7 +2285,7 @@ Pro ochranu vaší IP adresy, soukromé směrování používá vaše servery SMP k doručování zpráv. Použít web portu TCP port pro zprávy - Váš chat profil bude zaslán členům + Váš profil chatu bude zaslán členům chatu Režim systému Zmínky členů 👋 Organizujte konverzace do seznamů @@ -2331,7 +2328,7 @@ Použít TCP port %1$s, když není zadán žádný port. Tento odkaz byl použit s jiným mobilním zařízením, vytvořte na počítači nový odkaz. Získejte upozornění, když jste zmíněni. - SimpleX adresa a 1 rázové odkazy je bezpečné sdílet přes všechny komunikátory. + SimpleX adresa a jednorázové odkazy je bezpečné sdílet přes všechny komunikátory. Zprávy budou smazány pro všechny členy. Aplikace vyžaduje potvrzení stahování z neznámých serverů (s výjimkou .onion nebo při aktivaci SOCKS proxy). Musíte povolit kontaktům volání, abyste jim mohli zavolat. @@ -2367,7 +2364,6 @@ Členové budou odstraněny z chatu - toto nelze zvrátit! Použitím SimpleX chatu souhlasíte že:\n- ve veřejných skupinách budete zasílat pouze legální obsah.\n- budete respektovat ostatní uživatele – žádný spam. Přijmout - Nastavit operátora serveru Zásady ochrany soukromí a podmínky používání. Soukromé konverzace, skupiny a kontakty nejsou přístupné provozovatelům serverů. Nepodporovaný odkaz k připojení @@ -2550,4 +2546,14 @@ Připojení selhalo selhal Pokud jste se připojili k nějakým kanálům nebo je vytvořili, přestanou trvale fungovat. + aktivní + Narodili jste se bez účtu. + Nikdo nesledoval vaše konverzace. Nikdo nevytvořil mapu, kde jste byli. Soukromí nikdy nebylo funkcí - byl to způsob života. + Pak jsme se přesunuli na internet a každá platforma chtěla o vás něco vědět - vaše jméno, vaše číslo, vaše přátele. Smířili jsme se s tím, že cenou za komunikaci s ostatními je dát někomu vědět, s kým mluvíme. Každá generace, lidská i technická, to tak měla - telefon, e-mail, komunikátory, sociální sítě. Zdálo se, že je to jediný možný způsob. + Existuje i jiný způsob. Síť bez telefonních čísel. Bez uživatelských jmen. Bez účtů. Bez jakékoli uživatelské identity. Síť, která spojuje lidi a přenáší šifrované zprávy, aniž by bylo známo, kdo je připojen. + Nejde o to mít lepší zámek na dveřích někoho jiného. Ani o to mít nájemce, který respektuje vaše soukromí, ale vede evidenci všech vašich návštěvníků. Nejste host. Jste doma. Ani král k vám nemůže vstoupit - jste suverén. + Vaše konverzace patří vám, jako tomu bylo vždy před internetem. Síť není místo, které navštěvujete. Je to místo, které vytváříte a vlastníte. A nikdo vám ho nemůže vzít, ať už je soukromé, nebo veřejné. + Nejstarší lidská svoboda - mluvit s druhým člověkem, aniž by byl sledován - postavena na infrastruktuře, která ji nemůže zradit. + Protože jsme zničili sílu vědět, kdo jste. Aby vám vaši moc nikdo nemohl vzít. + Buďte svobodní ve své síti. 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 5d3d9ac90e..8700ade74e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -28,7 +28,7 @@ verbindung %1$d verbindung hergestellt für eine Verbindung eingeladen - verbinde… + Verbinde… sie haben einen Einmal-Link geteilt sie haben einen Einmal-Link inkognito geteilt über einen Gruppen-Link @@ -80,7 +80,7 @@ Der Server erfordert zum Erstellen von Warteschlangen eine Autorisierung. Bitte überprüfen Sie das Passwort. Fingerabdruck in der Serveradresse stimmt nicht mit dem Zertifikat überein. Verbinde - Erzeuge Warteschlange + Warteschlange erstellen Sichere Warteschlange Lösche Warteschlange Trenne Verbindung @@ -172,11 +172,11 @@ Willkommen! Dieser Text ist in den Einstellungen verfügbar. Chats - verbinde … + Verbinde… Sie sind zu der Gruppe eingeladen Beitreten als %s - verbinde … - Zum Starten eines neuen Chats tippen + Verbinde… + Tippen, um einen neuen Chat zu starten Chatten Sie mit den Entwicklern Sie haben keine Chats @@ -469,11 +469,10 @@ Sie entscheiden, wer sich mit Ihnen verbinden kann. Dezentral Jeder kann seine eigenen Server aufsetzen. - Erstellen Sie Ihr Profil + Ihr Profil erstellen Stellen Sie eine private Verbindung her Wie es funktioniert - Wie SimpleX funktioniert SimpleX nutzt individuelle Kennungen für jeden Ihrer Kontakte, um Ihre Privatsphäre zu schützen. Nur die Endgeräte speichern Benutzerprofile, Kontakte, Gruppen und Nachrichten. GitHub-Repository mehr dazu.]]> @@ -547,7 +546,7 @@ Privatsphäre App-Bildschirm schützen Bilder automatisch akzeptieren - Link-Vorschau senden + Linkvorschau senden App-Datensicherung MEINE DATEN @@ -695,7 +694,7 @@ Die Gruppeneinladung ist abgelaufen hat %1$s eingeladen. - verbunden + Verbunden hat die Gruppe verlassen änderte die Rolle von %s auf %s änderte Ihre Rolle auf %s @@ -726,13 +725,13 @@ Gruppe gelöscht Eingeladen Verbindung (erstellt) - Verbinde (nach einer Einladung) + Verbindung (nach einer Einladung) Verbindung (angenommen) Verbindung (angekündigt) Verbunden Vollständig Ersteller - verbinden + Verbinde Keine Kontakte zum Hinzufügen Neue Mitgliedsrolle @@ -757,7 +756,7 @@ Gruppe verlassen Gruppenprofil bearbeiten Gruppen-Link - Link erzeugen + Link erstellen Link löschen? Link löschen 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. @@ -944,7 +943,7 @@ Sicherheits-Gutachten Die Sicherheit von SimpleX Chat wurde von Trail of Bits überprüft. Was ist neu - Administratoren können Links für den Beitritt zu Gruppen erzeugen. + Administratoren können Links für den Beitritt zu Gruppen erstellen. Kontaktanfragen automatisch annehmen Vergleichen Sie die Sicherheitscodes mit Ihren Kontakten. App-Bildschirm in aktuellen Anwendungen verbergen. @@ -1040,7 +1039,7 @@ Für die Anzeige das Passwort im Suchfeld eingeben Privates Profil erzeugen! Stummschalten - Zum Aktivieren des Profils tippen. + Tippen, um das Profil zu aktivieren. Stummschaltung aufheben Bei Inaktivität stummgeschaltet! Schützen Sie Ihre Chat-Profile mit einem Passwort! @@ -1191,20 +1190,20 @@ Sie werden Ihre damit verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen. Design anpassen INTERFACE-FARBEN - Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet. + Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre SimpleX-Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre SimpleX-Kontakte gesendet. Alle Ihre Kontakte bleiben verbunden. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet. Erstellen Sie eine Adresse, damit sich Personen mit Ihnen verbinden können. SimpleX-Adresse erstellen - Mit Kontakten teilen + Mit SimpleX-Kontakten teilen Ihre Kontakte bleiben weiterhin verbunden. Automatisch akzeptieren Geben Sie eine Begrüßungsmeldung ein … (optional) Freunde einladen Lassen Sie uns über SimpleX Chat schreiben - Profil-Aktualisierung wird an Ihre Kontakte gesendet. + Profil-Aktualisierung wird an Ihre SimpleX-Kontakte gesendet. SimpleX-Adress-Einstellungen speichern Einstellungen speichern\? - Die Adresse mit Kontakten teilen\? + Die Adresse mit SimpleX-Kontakten teilen? Teilen beenden Das Teilen der Adresse beenden\? Keine Adresse erstellt @@ -1398,7 +1397,7 @@ Das Senden von Bestätigungen ist für %d Gruppen deaktiviert Das Senden von Bestätigungen ist für %d Gruppen aktiviert Für alle Gruppen deaktivieren - deaktiviert + Deaktiviert Bestätigungen sind deaktiviert Keine Information über die Zustellung Zustellung @@ -1438,7 +1437,7 @@ Datenbank-Ordner öffnen Das Passwort wird in Klartext in den Einstellungen gespeichert, nachdem Sie es geändert oder die App neu gestartet haben. Das Passwort wurde in Klartext in den Einstellungen gespeichert. - Bitte beachten Sie: Die Nachrichten- und Datei-Relais sind per SOCKS-Proxy verbunden. Anrufe und gesendete Link-Vorschaubilder nutzen eine direkte Verbindung.]]> + Bitte beachten Sie: Die Nachrichten- und Datei-Relais sind per SOCKS-Proxy verbunden. Anrufe nutzen eine direkte Verbindung.]]> Lokale Dateien verschlüsseln Öffnen Gespeicherte Dateien & Medien verschlüsseln @@ -1511,7 +1510,7 @@ Fehler bei der Neuverhandlung der Verschlüsselung Neuverhandlung der Verschlüsselung fehlgeschlagen Gruppenmitglieder blockieren - Erstellen Sie eine Gruppe mit einem zufälligen Profil. + Gruppe mit einem zufälligen Profil erstellen. Verbundener Desktop Desktop-Adresse Bessere Gruppen @@ -1607,7 +1606,7 @@ Kontakt hinzufügen Zum Scannen tippen Behalten - Zum Link einfügen tippen + Tippen, um den Link einzufügen Suchen oder SimpleX-Link einfügen Der Chat wurde gestoppt. Wenn diese Datenbank bereits auf einem anderen Gerät von Ihnen verwendet wurde, sollten Sie diese dorthin zurück übertragen, bevor Sie den Chat starten. Chat starten? @@ -1928,7 +1927,7 @@ Bitte überprüfen Sie, ob sich das Mobiltelefon und die Desktop-App im gleichen lokalen Netzwerk befinden, und die Desktop-Firewall die Verbindung erlaubt. \nBitte teilen Sie weitere mögliche Probleme den Entwicklern mit. Nachricht wurde nicht gesendet - Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt. + Diese Nachricht ist wegen der gewählten Chat-Präferenzen nicht erlaubt. Bitte versuchen Sie es später erneut. Fehler beim privaten Routing Die Nachricht kann später zugestellt werden, wenn das Mitglied aktiv wird. @@ -1971,7 +1970,7 @@ Abgelaufen Server-Einstellungen öffnen Andere Fehler - Proxy + Proxy-vermittelt Fehler beim Empfang Neu verbinden Deaktiviert @@ -1990,7 +1989,7 @@ Daten-Pakete heruntergeladen Verbundene Server Gelöscht - deaktiviert + Deaktiviert Fehler beim Wiederherstellen der Verbindung zum Server Nachricht weitergeleitet Dateien @@ -2001,7 +2000,7 @@ Dateispeicherort öffnen andere Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel: %1$s. - Link scannen / einfügen + Link einfügen / Scannen Alle Server neu verbinden Server neu verbinden? Alle Server neu verbinden? @@ -2108,7 +2107,6 @@ Erstellen Laden Sie neue Versionen von GitHub herunter. Direkt aus der Chat-Liste abspielen. - Chat-Liste umschalten: Kann von Ihnen in den Erscheinungsbild-Einstellungen geändert werden. Kontakte für spätere Chats archivieren. Ihre IP-Adresse und Verbindungen werden geschützt. @@ -2307,7 +2305,7 @@ Schutz der Privatsphäre Ihrer Kunden. Zur Verbindung aufgefordert Bitte verkleinern Sie die Nachrichten-Größe oder entfernen Sie Medien und versenden Sie diese erneut. - Nur Chat-Eigentümer können die Präferenzen ändern. + Präferenzen können nur von Chat-Eigentümern geändert werden. Bitte verkleinern Sie die Nachrichten-Größe und versenden Sie diese erneut. Die Rolle wird auf %s geändert. Im Chat wird Jeder darüber informiert. Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten. @@ -2447,15 +2445,14 @@ Moderatoren Mitglieder für Alle blockieren? Alle neuen Nachrichten dieser Mitglieder werden nicht angezeigt! - Durch die Nutzung von SimpleX Chat erklären Sie sich damit einverstanden:\n- nur legale Inhalte in öffentlichen Gruppen zu versenden.\n- andere Nutzer zu respektieren - kein Spam. + Sie verpflichten sich dazu:\n- nur legale Inhalte in öffentlichen Gruppen zu versenden.\n- andere Nutzer zu respektieren - kein Spam. Datenschutz- und Nutzungsbedingungen. Annehmen - Server-Betreiber konfigurieren - Private Chats, Gruppen und Ihre Kontakte sind für Server-Betreiber nicht zugänglich. + Betreiber verpflichten sich:\n- Unabhängig zu bleiben\n- Metadaten auf ein Minimum zu reduzieren\n- Geprüften Open‑Source‑Code einzusetzen Verbindungs-Link wird nicht unterstützt Verkürzter Link Vollständiger Link - SimpleX-Kanal-Link + SimpleX-Kanallink Für diesen Link wird eine neuere App-Version benötigt. Bitte aktualisieren Sie die App oder bitten Sie Ihren Kontakt einen kompatiblen Link zu senden. Alle Server Aus @@ -2605,7 +2602,7 @@ Link-Tracking entfernen Verbinden tippen, um den Bot zu nutzen. Um Befehle senden zu können, müssen Sie verbunden sein. - SimpleX Relais-Link + SimpleX Relais-Adresse Fehler beim Markieren als gelesen Fingerabdruck in der Zielserveradresse stimmt nicht mit dem Zertifikat überein: %1$s. Fingerabdruck in der Weiterleitungsserveradresse stimmt nicht mit dem Zertifikat überein: %1$s. @@ -2629,7 +2626,251 @@ Sprachnachrichten suchen Videos Sprachnachrichten - Verbindung fehlgeschlagen + VERBINDUNG FEHLGESCHLAGEN Fehlgeschlagen Kanäle, welche Sie erstellt haben oder denen Sie beigetreten sind, werden dauerhaft deaktiviert. + %1$d/%2$d Relais aktiv + %1$d/%2$d Relais aktiv, %3$d fehlgeschlagen + %1$d/%2$d Relais verbunden + %1$d/%2$d Relais verbunden, %3$d Fehler + %1$d Abonnent + %1$d Abonnenten + Angenommen + Aktiv + Abonnent für alle blockieren? + Broadcast + Kanalerstellung abbrechen? + Relais testen, um dessen Namen abzurufen.]]> + %1$s!]]> + Kanal + Kanal + Kanal + Kanallink + Kanal-Mitglieder + Kanalname + Der Kanal wird für alle Abonnenten gelöscht. Dies kann nicht rückgängig gemacht werden! + Der Kanal wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden! + Der Kanal wird mit %1$d von %2$d Relais gestartet. Fortfahren? + Chat-Relais + Chat-Relais + Chat-Relais + Chat-Relais + Chat‑Relais leiten Nachrichten in den von Ihnen erstellten Kanälen weiter. + Chat‑Relais leiten Nachrichten an Kanal-Abonnenten weiter. + Relais-Adresse überprüfen und erneut versuchen. + Relais-Name überprüfen und erneut versuchen. + Relais konfigurieren + Verbinden + Verbunden + Verbinde + Öffentlichen Kanal erstellen + Öffentlichen Kanal erstellen + Öffentlichen Kanal erstellen (BETA) + Kanal wird erstellt + Link dekodieren + Kanal löschen + Kanal löschen? + Gelöscht + Relais löschen + Kanalprofil bearbeiten + Aktiviere mindestens ein Chat‑Relais, um einen Kanal zu erstellen. + Relais-Name eingeben… + Fehler beim Hinzufügen des Relais + Fehler beim Erstellen des Kanals + Fehler beim Öffnen des Kanals + Fehlgeschlagen + Fehlgeschlagen + Link erhalten + Ungültige Relais-Adresse! + Ungültiger Relais-Name! + Eingeladen + Kanal beitreten + Kanal verlassen + Kanal verlassen? + Link + Neu + Neues Chat-Relais + Keine Chat-Relais + Es sind keine Chat-Relais aktiviert. + Es sind nicht alle Relais verbunden + Kanal öffnen + Neuen Kanal öffnen + EIGENTÜMER + Eigentümer + Voreingestellte Relais-Adresse + Voreingestellter Relais-Name + Relais + RELAIS + Relais-Adresse + Relais-Adresse + Relais-Verbindung fehlgeschlagen + Relais-Link + Relais-Test fehlgeschlagen! + Abonnent entfernen + Abonnent entfernen? + Der Server erfordert eine Autorisierung, um eine Verbindung zum Relais herzustellen. Bitte Passwort überprüfen. + Serverwarnung + Relais-Adresse teilen + ABONNENT + Abonnenten + Abonnenten verbinden sich über den Relais‑Link mit dem Kanal.\nDie Relais-Adresse wurde zur Einrichtung dieses Relais für diesen Kanal verwendet. + Abonnent wird aus dem Kanal entfernt. Dies kann nicht rückgängig gemacht werden! + Tippen, um dem Kanal beizutreten + Der Test ist bei Schritt %s fehlgeschlagen. + Relais testen + Dies ist eine Chat‑Relais-Adresse, welche nicht zum Verbinden verwendet werden kann. + Abonnent für alle freigeben? + Für neue Kanäle verwenden + Relais nutzen + Überprüfen + via %1$s + Sprachaufnahmen werden auf Ihrer Plattform nicht unterstützt. + Abwarten + Antwort abwarten + Sie + Sie sind Abonnent + Sie können einen Link oder QR-Code teilen - damit kann jeder dem Kanal beitreten. + Sie haben sich über diesen Relais‑Link mit dem Kanal verbunden. + Ihr Kanal + Ihr Kanal + Ihr Profil %1$s wird mit den Kanal‑Relais und -Abonnenten geteilt.\nRelais können auf Kanalnachrichten zugreifen. + Ihre Relais-Adresse + Ihr Relais-Name + Sie werden keine Nachrichten mehr aus diesem Kanal erhalten. Der Chatverlauf bleibt erhalten. + Vollständiger Name des Kanals: + Das Kanalprofil wird auf den Geräten der Abonnenten und auf den Chat‑Relais gespeichert. + Kanalprofil wurde aktualisiert + %d Kanalereignisse + Kanal gelöscht + Verworfen (%1$d Versuche) + Fehler: %s + Fehler beim Speichern des Kanalprofils + Übertragungsfehler + Speichern und Abonnenten des Kanals informieren + Kanalprofil speichern + Die App hat diese Nachricht nach %1$d Empfangsversuchen entfernt. + Kanalprofil aktualisiert + %1$d/%2$d Relais aktiv, %3$d Fehler + %1$d/%2$d Relais aktiv, %3$d entfernt + %1$d/%2$d Relais verbunden, %3$d fehlgeschlagen + %1$d/%2$d Relais verbunden, %3$d entfernt + %1$d Relais fehlgeschlagen + %1$d Relais nicht aktiv + %1$d Relais entfernt + Das Hinzufügen von Relais wird zu einem späteren Zeitpunkt unterstützt. + Alle Relais fehlgeschlagen + Alle Relais entfernt + Broadcast nicht möglich + Der Kanal hat keine aktiven Relais. Bitte später erneut versuchen. + Der Kanal ist vorübergehend nicht erreichbar + Inaktiv + Keine aktiven Relais + Vom Betreiber entfernt + Warte auf das Hinzufügen von Relais durch den Eigentümer des Kanals. + Geschäftliche Adresse + Kanallink + Kontaktadresse + Deaktivieren + Aktivieren + Linkvorschau aktivieren? + Fehler + Fehler beim Teilen des Kanals + (vom Eigentümer) + Gruppen-Link + Linksignatur erfolgreich überprüft. + Netzwerk-Fehler + Einmal-Link + Relay‑Status: + Das Senden einer Link-Vorschau kann Ihre IP‑Adresse an die Website übermitteln. Sie können dies später in den Datenschutzeinstellungen ändern. + Kanal teilen… + Per Chat teilen + ⚠️ Signaturüberprüfung fehlgeschlagen: %s. + (signiert) + Zum Öffnen tippen + Die Verbindung hat das Limit für nicht zugestellte Nachrichten erreicht + Kanal-Präferenzen + Kanal-Präferenzen können nur von Kanal-Eigentümern geändert werden. + Verbindungs-Link für eine Person + Kanäle + Über einen Link oder QR-Code verbinden + Ihren Link erstellen + Ihre öffentliche Adresse erstellen + Freunde einladen – jetzt noch einfacher 👋 + Damit Sie jeder erreichen kann + Für privaten Chat einladen + Jemand mit Ihnen verbinden lassen + Neuer Einmal-Link + Non‑Profit‑Governance + - Opt‑in zum Senden von Linkvorschauen.\n- SOCKS-Proxy verwenden, falls aktiviert.\n- Hyperlink‑Phishing verhindern.\n- Link‑Tracking entfernen. + Oder den QR‑Code persönlich oder per Videoanruf zeigen. + Oder diesen QR‑Code verwenden – ausgedruckt oder online. + Volle Kontrolle: Sie können Ihre eigenen Relais betreiben. + Privatsphäre: für Besitzer und Abonnenten. + Öffentliche Kanäle – frei sprechen 🚀 + Zuverlässigkeit: mehrere Relais pro Kanal. + Sichere Web-Links + Sicherheit: Eigentümer besitzen die Kanalschlüssel. + Den Link über einen beliebigen Messenger versenden – es ist sicher. Bitte in SimpleX einfügen. + Mit Jemandem sprechen + Für ein dauerhaftes SimpleX-Netzwerk. + Diese Adresse in Ihrem Social‑Media‑Profil, auf Ihrer Webseite oder in Ihrer E‑Mail‑Signatur verwenden. + Wir haben das Verbinden für neue Nutzer vereinfacht. + Ihre öffentliche Adresse + Sie wurden ohne eine Benutzerkennung geboren. + Niemand verfolgte Ihre Gespräche. Niemand erstellte eine Karte, wo Sie sich aufgehalten haben. Privatsphäre war nie ein Feature - sie war selbstverständlich. + Dann sind wir online gegangen, und jede Plattform wollte Etwas von Ihnen - Ihren Namen, Ihre Nummer, Ihre Freunde. Wir akzeptierten, dass es der Preis mit Anderen zu kommunizieren ist, Jemandem preiszugeben, mit wem und wie wir miteinander kommunizieren. Jede Generation, Menschen und Technologien, kannten es nur so - Telefon, E-Mail, Messenger, soziale Medien. Es schien der einzig mögliche Weg zu sein. + Es gibt einen anderen Weg. Ein Netzwerk ohne Telefonnummern, ohne Benutzernamen, ohne Benutzerkennungen und ohne jegliche Benutzeridentität. Ein Netzwerk, welches Menschen verbindet und verschlüsselte Nachrichten überträgt, ohne zu wissen, wer mit wem verbunden ist. + Nicht ein besseres Schloss an der Tür eines Anderen. Kein freundlicher Vermieter, der Ihre Privatsphäre respektiert, aber dennoch jeden Besucher registriert. Sie sind kein Gast. Sie sind zu Hause. Kein Vermieter, kein Fremder kann es betreten - Sie sind souverän. + Ihre Kommunikation gehört Ihnen, so wie es immer war, bevor es das Internet gab. Das Netzwerk ist kein Ort, den Sie besuchen. Es ist ein Ort, den Sie erschaffen und besitzen und Niemand kann es Ihnen nehmen, egal ob Sie es privat oder öffentlich machen. + Die älteste Freiheit des Menschen - mit einem anderen Menschen sprechen zu können, ohne beobachtet zu werden - gestützt auf einer Infrastruktur, die Sie nicht verraten kann. + Weil wir die Macht zerstört haben, zu wissen, wer Sie sind. Damit Ihnen Ihre Macht niemals genommen werden kann. + Genießen Sie die Freiheit in Ihrem Netzwerk. + + Abonnenten-Meldungen + Das Senden von Direktnachrichten an Abonnenten erlauben. + Das Senden von Direktnachrichten an Abonnenten nicht erlauben. + Bis zu 100 der letzten Nachrichten an neue Abonnenten senden. + Den Nachrichtenverlauf nicht an neue Abonnenten senden. + Abonnenten können verschwindende Nachrichten versenden. + Abonnenten können Direktnachrichten versenden. + Direktnachrichten zwischen Abonnenten sind nicht erlaubt. + Abonnenten können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden) + Abonnenten können eine Reaktion auf Nachrichten geben. + Abonnenten können Sprachnachrichten versenden. + Abonnenten können Dateien und Medien versenden. + Abonnenten können SimpleX-Links versenden. + Abonnenten können Nachrichten an Moderatoren melden. + Bis zu 100 der letzten Nachrichten werden an neue Abonnenten gesendet. + Der Nachrichtenverlauf wird nicht an neue Abonnenten gesendet. + Mitgliedern den Chat mit Administratoren erlauben. + Abonnenten den Chat mit Administratoren erlauben. + Seien Sie frei\nin Ihrem Netzwerk + Chats mit Administratoren sind nicht erlaubt. + Chats mit Administratoren in öffentlichen Kanälen sind nicht Ende‑zu‑Ende‑verschlüsselt – bitte nur über vertrauenswürdige Chat‑Relais nutzen. + Chats mit Mitgliedern sind deaktiviert + Chat mit Administratoren + Aktivieren + Chats mit Administratoren aktivieren? + Geben Sie einen Profilnamen ein… + Jetzt starten + Mitglieder können mit Administratoren chatten. + nicht Ende‑zu‑Ende‑verschlüsselt. Chat‑Relais können sie einsehen.]]> + Migrieren + Netzwerk‑Verpflichtungen + Netzwerk‑Router können nicht erkennen,\nwer mit wem kommuniziert + Kein Account. Keine Telefonnummer. Keine E‑Mail. Keine ID.\nDie sicherste Verschlüsselung. + Auf Ihrem Gerät, nicht auf Servern. + Externen Link öffnen? + Private und sichere Kommunikation. + Chat mit Administratoren nicht erlauben. + Benachrichtigungen einrichten + Router einrichten + Abonnenten können mit Administratoren chatten. + Das erste Netzwerk,\nin dem Sie Ihre Kontakte und Gruppen besitzen. + Warum SimpleX entwickelt wurde. + Ihr Netzwerk + Ihr Profil + Untere Leiste + Die Linkvorschau wird über einen SOCKS-Proxy angefordert. DNS-Abfragen können dennoch lokal über Ihren DNS-Resolver erfolgen. + Obere Leiste diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml index 20a271f58f..47cfd90ad6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml @@ -549,7 +549,6 @@ Διαμορφωμένοι SMP διακομιστές Διαμορφωμένοι XFTP διακομιστές Διαμορφωμένοι ICE διακομιστές - Διαμόρφωση χειριστών διακομιστή Επιβεβαίωσε Επιβεβαίωση διαγραφής επαφής; Επιβεβαίωση αναβαθμίσεων βάσης δεδομένων @@ -1007,7 +1006,6 @@ Πως επηρεάζει τη μπαταρία Πως βοηθάει την ιδιωτικότητα Πως δουλεύει - Πως δουλεύει το SimpleX Πως να Πως να το χρησιμοποιήσεις Πως να χρησιμοποιήσεις markdown σύνταξη @@ -1387,7 +1385,6 @@ Για να λαμβάνεις ειδοποιήσεις σχετικά με τις νέες εκδόσεις, ενεργοποίησε τον περιοδικό έλεγχο για σταθερές ή δοκιμαστικές εκδόσεις. Για να συνδεθείς μέσω συνδέσμου Για να συνδεθείς, η επαφή σου μπορεί να σαρώσει τον κωδικό QR ή να χρησιμοποιήσει τον σύνδεσμο στην εφαρμογή. - Εναλλαγή λίστας συνομιλιών: Ενεργοποίηση ανώνυμης λειτουργίας κατά τη σύνδεση. Για απόκρυψη ανεπιθύμητων μηνυμάτων. Για να πραγματοποιήσεις κλήσεις, επέτρεψε τη χρήση του μικροφώνου σου. Τερμάτισε την κλήση και προσπάθησε να καλέσεις ξανά. 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 c233d8eabc..7088c54d9b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -358,7 +358,7 @@ Los miembros pueden enviar mensajes de voz. en modo incógnito mediante dirección de contacto ¡Error al crear perfil! - No se pudo cargar el chat + Fallo en la carga del chat Fallo en la carga de chats Enlace completo Error al eliminar contacto @@ -381,11 +381,10 @@ Servidores ICE (uno por línea) Nombre completo: Tu decides quién se conecta. - Cómo funciona SimpleX Colgar Archivos y multimedia ¡Grupo no encontrado! - perfil de grupo actualizado + perfil del grupo actualizado Error al crear enlace de grupo Error al eliminar enlace de grupo activado para el contacto @@ -819,7 +818,7 @@ Cambiar servidor de recepción Totalmente descentralizado. Visible sólo para los miembros. Para conectarte mediante enlace - ¡Prueba no superada! + ¡Prueba del servidor no superada! Algunos servidores no han superado la prueba: Usar servidor Para conexiones nuevas @@ -880,7 +879,7 @@ Has sido invitado a un grupo. Únete para conectar con sus miembros. has expulsado a %1$s Tú: %1$s - Puedes compartir el enlace o el código QR para que cualquiera pueda unirse al grupo. Si más tarde lo eliminas, no afectará a los miembros del grupo. + Puedes compartir el enlace o código QR. Cualquiera podrá unirse al grupo. Si más tarde lo eliminas, no afectará a los miembros del grupo. Cuando compartes un perfil incógnito con alguien, este perfil también se usará para los grupos a los que te inviten. Mis preferencias Con mensaje de bienvenida opcional. @@ -1117,7 +1116,7 @@ Fondo Exportar tema Menús y alertas - 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. + Añade la dirección a tu perfil para que tus contactos SimpleX puedan compartirla con otros. La actualización del perfil se enviará a tus contactos SimpleX. Acerca de la dirección SimpleX Dirección Todos tus contactos permanecerán conectados. La actualización del perfil se enviará a tus contactos. @@ -1134,7 +1133,7 @@ Invitar amigos Hablemos en SimpleX Chat Asegúrate de que el archivo tiene la sintaxis YAML correcta. Exporta el tema para tener un ejemplo de la estructura del archivo de tema. - La actualización del perfil se enviará a tus contactos. + La actualización del perfil se enviará a tus contactos SimpleX. Mensaje recibido Guardar configuración de dirección SimpleX Puedes compartir tu dirección como enlace o código QR para que cualquiera pueda conectarse contigo. @@ -1145,8 +1144,8 @@ ¿Dejar de compartir la dirección\? COLORES DE LA INTERFAZ Puedes crearla más tarde - ¿Compartir la dirección con los contactos\? - Compartir con contactos + ¿Compartir la dirección con los contactos SimpleX? + Compartir con contactos SimpleX Título Puedes compartir esta dirección con tus contactos para que puedan conectar con %s. Tus contactos permanecerán conectados. @@ -1364,7 +1363,7 @@ Abrir Cifra archivos almacenados y multimedia Error al establecer contacto con el miembro - Recuerda: los servidores están conectados mediante proxy SOCKS, pero las llamadas y las previsualizaciones de enlaces usan conexión directa.]]> + Recuerda: los servidores están conectados mediante proxy SOCKS. Las llamadas usan conexión directa.]]> Cifrar archivos locales Nueva aplicación para ordenador! 6 nuevos idiomas para la interfaz @@ -1430,7 +1429,7 @@ Conectar con ordenador Desconectar ¿Bloquear miembro? - %d evento(s) de grupo + %d evento(s) del grupo ¡Nombre no válido! Conectado a móvil Dirección ordenador incorrecta @@ -1909,7 +1908,7 @@ Aún no hay conexión directa, los mensajes son reenviados por el administrador. Otros servidores SMP Otros servidores XFTP - Escanear / Pegar enlace + Pegar enlace / Escanear Mostrar porcentaje Desactivar Desactivado @@ -2041,7 +2040,6 @@ Ningún contacto filtrado Difumina para mayor privacidad Crear - Alternar lista de chats: Ajusta el tamaño de la fuente. Reproduce desde la lista de chats. Actualizar la aplicación automáticamente @@ -2223,7 +2221,7 @@ Sólo los propietarios del chat pueden cambiar las preferencias. El miembro será eliminado del chat. ¡No puede deshacerse! El rol cambiará a %s. Se notificará en el chat. - Dejarás de recibir mensajes del chat. El historial del chat se conserva. + Dejarás de recibir mensajes del chat. El historial del chat se conservará. Cómo ayuda a la privacidad Cuando está habilitado más de un operador, ninguno dispone de los metadatos para conocer quién se comunica con quién. Tu perfil de chat será enviado a los miembros de chat @@ -2374,9 +2372,8 @@ La frase de contraseña no se ha podido leer en Keystore. Por favor, introdúcela manualmente. Puede deberse a alguna actualización del sistema incompatible con la aplicación. Si no es así, por favor, ponte en contacto con los desarrolladores. Aceptar Política de privacidad y condiciones de uso. - Los chats privados, los grupos y tus contactos no son accesibles para los operadores de servidores. - Al usar SimpleX Chat, aceptas:\n- enviar únicamente contenido legal en los grupos públicos.\n- respetar a los demás usuarios - spam prohibido. - Configurar operadores de servidores + Los operadores se comprometen a:\n- Ser independientes\n- Minimizar el tratamiento de metadatos\n- Ejecutar código open-source verificado + Te comprometes a:\n- Sólo contenido legal en grupos públicos \n- Respetar a los demás usuarios — no hacer spam Enlace de canal SimpleX Enlace completo Enlace corto @@ -2530,7 +2527,7 @@ Abrir enlace limpio Limpiar enlaces de seguimiento Abrir enlace completo - Enlace de servidor SimpleX + Dirección de servidor SimpleX Error al marcar como leído La huella en la dirección del servidor no coincide con el certificado: %1$s. La huella en la dirección del servidor de destino no coincide con el certificado: %1$s. @@ -2554,4 +2551,251 @@ Buscar mensajes de voz Vídeos Mensajes de voz + %1$d/%2$d servidores activos + %1$d/%2$d servidores activos, %3$d con fallo + %1$d/%2$d servidores conectados + %1$d/%2$d servidores conectados, %3$d errores + aceptado + activo + Test del servidor para recibir su nombre.]]> + El perfil del canal se almacena en los dispositivos de los suscriptores y en los servidores de chat. + El canal comenzará a funcionar con %1$d de %2$d servidores. ¿Continuar? + Servidores de chat + Servidores de chat + Servidores de chat + Servidores de chat + Los servidores de chat reenvían los mensajes en los canales que has creado. + Los servidores de chat reenvían los mensajes a los suscriptores del canal. + Comprueba la dirección del servidor y prueba de nuevo. + Comprueba el nombre del servidor y prueba de nuevo. + Configurar servidores + Conectar + conectado + conectando + Decodificar enlace + eliminado + Eliminar servidor + Activa al menos un servidor de chat para crear un canal. + Introduce el nombre del servidor… + Error al añadir el servidor + La conexión con el servidor ha fallado + ¡El test del servidor ha fallado! + Prueba no superada en el paso %s. + %1$d suscriptor + %1$d suscriptores + ¿Bloquear al suscriptor para todos? + Retransmisión + ¿Cancelar la creación del canal? + %1$s!]]> + canal + Canal + Canal + Título completo: + Enlace del canal + Miembros canal + Título del canal + perfil del canal actualizado + El canal será eliminado para todos los suscriptores. ¡No puede deshacerse! + El canal será eliminado para tí. ¡No puede deshacerse! + CONEXIÓN FALLIDA + Crear canal público + Crear canal público + Crear canal público (BETA) + Creando canal + %d eventos del canal + Eliminar canal + ¿Eliminar el canal? + canal eliminado + caído (%1$d intentos) + Editar perfil del canal + Error al crear el canal + Error al abrir el canal + error:%s + Error al guardar el perfil del canal + fallo + fallo + Recibir el enlace + Si te has unido o has creado canales, dejarán de funcionar permanentemente. + ¡Dirección de servidor no válido! + ¡Nombre de servidor no válido! + Invitado + Unirme al canal + Salir del canal + ¿Salir del canal? + Enlace + Mensaje de error + nuevo + Nuevo servidor de chat + Sin servidores de chat + Ningún servidor de chat activado. + Hay servidores no conectados + Abrir canal + Abrir canal nuevo + PROPIETARIO + Propietarios + Direcciones predefinidas + Nombres predefinidos + servidor + SERVIDOR + Dirección servidor + Dirección del servidor + Enlace servidor + Eliminar suscriptor + ¿Eliminar suscriptor? + Guardar y notificar suscriptores + Guardar perfil del canal + El servidor requiere autorización para conectar con el servidor, comprueba la contraseña. + Alerta del servidor + Compartir dirección del servidor + SUSCRIPTOR + Suscriptores + Los suscriptores usan el enlace del servidor para conectarse a los canales.\nLa dirección del servidor se usó para establecer el servidor para el canal. + El suscriptor será eliminado del canal. ¡No puede deshacerse! + Pulsa Unirme al canal + Test servidor + La app ha eliminado el mensaje tras %1$d intentos de recibirlo. + Esto es una dirección de servidor, no puede usarse para conectar. + ¿Desbloquear al suscriptor para todos? + perfil del canal actualizado + Usar para canales nuevos + Usar servidor + Verificar + mediante %1$s + La grabación de voz no es compatible con tu plataforma + Espera + Espera respuesta + tu + eres suscriptor + Puedes compartir un enlace o código QR. Cualquiera podrá unirse al canal. + Te conectaste al canal mediante este enlace de servidor. + Tu canal + Tu canal + El perfil %1$s será compartido con los servidores de canal y los suscriptores.\nLos servidores tienen acceso a los mensajes del canal. + Tu dirección de servidor + Tu nombre del servidor + Dejarás de recibir mensajes de este canal. El historial del chat se conservará. + fallo + Naciste sin una cuenta. + Nadie monitorizaba tus conversaciones. Nadie registraba tus ubicaciones. La privacidad nunca fue un lujo, era la manera de vivir. + Después pasamos a internet y cada plataforma pedía una parte de tí: tu nombre, tu número, tus amistades. Aceptamos que el precio de hablar con los demás es informar a alguien de quién es interlocutor. Cada generación, personas y tecnología, ha funcionado así: teléfono, email, mensajería, redes sociales. Parecía el único camino. + Existe otro camino. Una red sin números de teléfono. Sin nombres de usuario. Sin cuentas. Sin identificadores de ningún tipo. Una red que conecta las personas y entrega mensajes cifrados sin saber quien está conectado. + No un candado mejorado en la puerta de otro. No un terrateniente que respeta tu privacidad pero sigue guardando un registro de tus visitantes. Tu no eres el invitado. Estás en tu casa y ningún rey podrá entrar. Tu eres el soberano. + Tus conversaciones te pertenecen, tal como ha sido siempre antes de la llegada de internet. Tu red no es un lugar que visitas. Es un lugar que has creado, te pertenece y nadie te la podrá quitar, ya sea pública o privada. + La libertad más antigua del ser humano, la de hablar con otra persona sin ser observado, materializada sobre una infraestructura que no puede traicionarla. + Porque hemos destruido el poder de saber quien eres. De manera que tu poder nunca se pueda arrebatar. + Se libre en tu red. + + Informes de suscriptores + Se permiten mensajes directos entre suscriptores. + No se permiten mensajes directos entre suscriptores. + Se envían hasta 100 mensajes más recientes a los suscriptores nuevos. + No se envía el historial a los suscriptores nuevos. + Los suscriptores del canal pueden enviar mensajes temporales. + Los suscriptores del canal pueden enviar mensajes directos. + Los mensajes directos entre suscriptores del canal no están permitidos. + Los suscriptores del canal pueden eliminar mensajes de forma irreversible. (24 horas) + Los suscriptores pueden añadir reacciones a los mensajes. + Los suscriptores del canal pueden enviar mensajes de voz. + Los suscriptores del canal pueden enviar archivos y multimedia. + Los suscriptores del canal pueden enviar enlaces SimpleX. + Los suscriptores pueden informar de mensajes a los moderadores. + Hasta 100 últimos mensajes son enviados a los suscriptores nuevos. + El historial no se envía a suscriptores nuevos. + %1$d/%2$d servidores activos, %3$d errores + %1$d/%2$d servidores activos, %3$d servidores eliminados + %1$d/%2$d servidores conectados, %3$d con fallo + %1$d/%2$d servidores conectados, %3$d eliminados + %1$d servidores han fallado + %1$d servidores inactivos + %1$d servidores eliminados + Añadir servidores estará disponible en una versión posterior. + Enlace para un solo contacto + Permitir que los miembros chateen con administradores. + Permitir que los suscriptores chateen con administradores. + Todos los servidores han fallado + Todos los servidores eliminados + Se libre\nen tu red + Dirección empresarial + no puedes retransmitir + El canal no tiene servidores activos. Por favor, intenta unirte más tarde. + Enlace del canal + Preferencias del canal + Canales + Canales no disponibles temporalmente + Chat con administradores no permitido + El chat con administradores en el canal público no dispone de cifrado E2E. Úsalo sólo con servidores de confianza. + Chats con miembros desactivado + Chat con administradores + Conecta vía enlace o QR + Crea tu enlace + Dirección de contacto + Crea tu dirección pública + Desactivar + Invitar a tus amigos es más fácil 👋 + Activar + Activar + ¿Activar chat con administradores? + ¿Activar previsualización de enlaces? + Introduce el nombre del perfil… + Error + Error al compartir el canal + Para que cualquiera acceda a ti + (del propietario) + Empezar + Enlace de grupo + inactivo + Invitación privada + Conecta con alguien + Firma del enlace verificada + Los miembros pueden chatear con los administradores + Migrar + Error de red + Los routers de la red no pueden saber\nquién se comunica con quién + Nuevo enlace de 1 solo uso + Sin cuenta. Sin teléfono. Sin email. Sin ID.\nEl cifrado más seguro. + Sin servidores activos + Enlace de un solo uso + Sólo los propietarios pueden modificar las preferencias de los canales. + En tu teléfono, no en el servidor. + ¿Abrir enlace externo? + O muestra el código QR en persona o por videollamada. + O usa el QR, imprímelo o muestralo en línea. + En propiedad: puedes poner en marcha tus propios servidores. + Privacidad: para propietarios y suscriptores. + Mensajería segura y privada. + El chat con los administradores no está permitido. + Canales públicos - habla con libertad 🚀 + Resultados del servidor: + Fiabilidad: muchos servidores por canal. + eliminado por el operador + Enlaces web seguros + Seguridad: los propietarios tienen la llave del canal. + Enviar una previsualización del enlace puede revelar tu dirección IP al sitio web. Puedes cambiarlo más tarde en los ajustes de privacidad. + Envía el enlace con cualquier mensajero, es seguro. El contacto debe pegarlo en SimpleX. + Configurar notificaciones + Configurar routers + Compartir canal… + Compartir mediante chat + ⚠️ Verificación de firma fallida: %s. + (firmado) + Los suscriptores pueden chatear con los administradores. + Para comunicarte + Pulsa para abrir + La conexión ha alcanzado al límite de mensajes no entregados + La primera red donde los grupos\ny los contactos son tuyos. + Para que la Red SimpleX perdure. + Usa esta dirección en el perfil de tus redes sociales, página web o firma email. + Esperando a que el propietario del canal añada servidores. + Hemos simplificado la conexión para los usuarios nuevos. + Por qué fue creado SimpleX. + Tu red + Tu perfil + Tu dirección pública + no están cifrados de extremo a extremo. Los servidores pueden ver estos mensajes.]]> + Compromisos en la red + Gobernanza no lucrativa + - aceptar el envío de vistas previas de los enlaces. \n- usar proxy SOCKS si está hablilitado.\n- prevenir el phishing mediante hipervínculos. \n- eliminar el seguimiento de los enlaces. + Menú inferior + Las previsualizaciones de enlaces se solicitan a través del proxy SOCKS. Las peticiónes DNS aún pueden usar el DNS local del sistema. + Menú superior diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml index 5483becb91..3f7d4ff025 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -730,7 +730,6 @@ تماس پذیرفته نامتمرکز پروفایل خود را ایجاد کنید - SimpleX چگونه کار می‌کند مخزن GitHub ما.]]> استفاده از چت بهترین گزینه برای باتری. شما اعلان‌ها را فقط وقتی دریافت می‌کنید که برنامه در حال اجراست (بدون سرویس پس‌زمینه).]]> @@ -2183,7 +2182,6 @@ شرایط به‌طور خودکار برای اپراتورهای فعال در: %s پذیرفته خواهد شد. سرورهای SMP پیکربندی‌شده سرورهای XFTP پیکربندی‌شده - پیکربندی اپراتورهای سرور سرورهای متصل شده اتصال نیاز به تجدید مذاکره رمزنگاری دارد. اتصالات @@ -2426,7 +2424,6 @@ این پیام حذف شده یا هنوز دریافت نشده است. زمان ناپدید شدن فقط برای مخاطبان جدید تنظیم شده است. برای مطلع شدن از نسخه‌های جدید، بررسی دوره‌ای برای نسخه‌های Stable یا Beta را فعال کنید. - تغییر لیست چت: برای برقراری تماس، اجازه دهید از میکروفن شما استفاده شود. تماس را پایان دهید و دوباره تلاش کنید. برای جلوگیری از جایگزینی لینک شما، می‌توانید کدهای امنیتی مخاطب را مقایسه کنید. برای دریافت 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 c6b094ab56..24634192ec 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml @@ -388,7 +388,6 @@ Miten Koko nimi: Miten markdownia käytetään - Miten SimpleX toimii Saapuva äänipuhelu Saapuva videopuhelu Sivuuta @@ -822,7 +821,7 @@ Arvioi sovellus Skannaa palvelimen QR-koodi Profiilipäivitys lähetetään kontakteillesi. - Jaa osoite kontakteille\? + Jaa osoite kontakteille? Lopeta jakaminen Lopeta osoitteen jakaminen\? Tallenna automaattisen hyväksynnän asetukset 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 563b1a02c7..d95f8ad500 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -85,7 +85,7 @@ Les notifications périodiques sont désactivées ! Une phrase secrète est nécessaire Autoriser le dans la boîte de dialogue suivante pour recevoir des notifications instantanément.]]> - Le serveur requiert une autorisation pour créer des files d\'attente, vérifiez le mot de passe + Le serveur requiert une autorisation pour créer des files d\'attente, vérifiez le mot de passe. 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. SimpleX fonctionne en arrière-plan au lieu d\'utiliser les notifications push.]]> Cacher @@ -447,7 +447,6 @@ Créez votre profil Établir une connexion privée Comment ça fonctionne - Comment SimpleX fonctionne Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages. GitHub repository.]]> Batterie peu utilisée. L\'app vérifie les messages toutes les 10 minutes. Vous risquez de manquer des appels ou des messages urgents.]]> @@ -1017,7 +1016,7 @@ Supprimer le fichier Erreur lors de la sauvegarde des serveurs XFTP Assurez-vous que les adresses des serveurs XFTP sont au bon format, séparées par des lignes et qu\'elles ne sont pas dupliquées. - Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe + Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe. Téléverser le fichier Serveurs XFTP Vos serveurs XFTP @@ -1056,7 +1055,7 @@ Système Authentification annulée ID du message incorrect - Le hash du message précédent est différent.\" + Le hash du message précédent est différent. L\'ID du message suivant est incorrect (inférieur ou égal au précédent). \nCela peut se produire en raison d\'un bug ou lorsque la connexion est compromise. Erreur de déchiffrement @@ -1105,7 +1104,7 @@ Vous pouvez accepter ou refuser les demandes de contacts. COULEURS DE L\'INTERFACE Vos contacts resteront connectés. - Partager l\'adresse avec vos contacts \? + Partager l\'adresse avec vos contacts ? Partager avec vos contacts Entrez un message de bienvenue… (facultatif) Cesser le partage @@ -1149,7 +1148,7 @@ Assurez-vous que le fichier a une syntaxe YAML correcte. Exporter le thème pour avoir un exemple de la structure du fichier du thème. La mise à jour du profil sera envoyée à vos contacts. Guide de l\'utilisateur.]]> - Enregistrer les paramètres de validation automatique + Enregistrer les paramètres de l\'adresse SimpleX Pour se connecter, votre contact peut scanner un code QR ou utiliser un lien dans l\'app. Le code d\'accès de l\'application est remplacé par un code d\'autodestruction. Activer l\'autodestruction @@ -1950,7 +1949,7 @@ Infos serveurs Afficher les informations pour À partir de %s. - À partir de %s. \nToutes les données restent confinées dans votre appareil. + À partir de %s.\nToutes les données restent confinées dans votre appareil. Statistiques Total Serveur XFTP @@ -2042,7 +2041,6 @@ %d sélectionné(s) Aperçu depuis la liste de conversation. Les messages seront marqués comme modérés pour tous les membres. - Afficher la liste des conversations : Vous pouvez choisir de le modifier dans les paramètres d\'apparence. Rétablir tous les conseils Mise à jour automatique de l\'app @@ -2361,4 +2359,90 @@ Quatres nouvelles langues d\'interface Partagez votre adresse Actualisez votre adresse + 1 discussion avec un membre + vous a accepté + Accepter le membre + actif + tous + Tous les messages + Permettre des fichiers et des médias seulement si votre contact les permet. + Permettre à vos contacts d\'envoyer des fichiers et des médias. + Tous les serveurs + seulement après que votre requête soit acceptée.]]> + Vérifiez l\'adresse de relais et essayez à nouveau. + Vérifiez le nom du relais et essayez à nouveau. + Se connecter + Se connecter + connecté + CONNEXION ÉCHOUÉE + contact supprimé + contact désactivé + le contact devrait accepter… + Créez votre adresse + supprimé + Supprimer les messages + Supprimer le relais + %d messages + Entrez le nom de relais.. + échoué + échoué + Fichiers + Les membres seront retirés du groupe - impossible de revenir en arrière! + Le membre va rejoindre le groupe, accepter le membre? + Les messages de ces membres seront affichés! + modérateurs + nouveau + Nouveau rôle de groupe: Modérateur + non synchronisé + Désactivé + Ouvrir pour se connecter + Ouvrez pour rejoindre + en attente + en attente d\'approbation + Veuillez attendre que les modérateurs de groupe examinent votre demande pour rejoindre le groupe. + Adresse de relais prédéfinie + Nom de relais prédéfini + Serveurs prédéfinis + Politique de confidentialité et conditions d\'utilisation. + Rejeter + Rejeter la demande de contact + rejeté + rejeté + Rejeter le membre? + relais + RELAIS + Adresse de relais + Adresse de relais + Échec de la connexion au relais + Lien du relais + Retirer et supprimer les messages + retiré du groupe + Retirer les membres? + Retire les messages et bloque les membres. + Rapport envoyé aux modérateurs + requête envoyée + examiné par les administrateurs + Examiner les membres du groupe + Examiner les membres + Envoyez une demande de contact? + Envoyer la demande + Envoyer la demande sans message + Envoyez vos commentaires privés aux groupes. + Envoyé à votre contact après la connexion. + Le serveur requiert une autorisation pour se connecter au relais, vérifiez le mot de passe. + Avertissement du serveur + Partager l\'ancienne adresse + Partager l\'ancien lien + Partager l\'adresse de relais + Description courte: + Lien court + Adresse SimpleX courte + Adresse du relais SimpleX + Port TCP pour la messagerie + Échec du test à l\'étape %s. + Tester le relais + L\'adresse sera courte et votre profil sera partagé via l\'adresse. + Le lien sera court, et le profil de groupe sera partagé via le lien. + L\'expéditeur n\'en sera PAS informé. + Ce réglage est pour votre profil actuel diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml index 3084b8569b..84e806dda0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml @@ -90,7 +90,6 @@ Domaćin Kako utiče na bateriju Kako pomaže privatnosti - Kako SimpleX radi Grupni profil je uskladnjen na uredjajima korisnika, ne na serverima. O operatorima Skrivena šifra profila diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 09a843c91c..2df64ae590 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -29,7 +29,7 @@ Elfogadja a kapcsolódási kérést? Elfogadás Elfogadás - Cím hozzáadása a profilhoz, hogy a partnerei megoszthassák másokkal. A profilfrissítés el lesz küldve partnerei számára. + Cím hozzáadása a profilhoz, hogy a SimpleX partnerei megoszthassák másokkal. A profilfrissítés el lesz küldve a SimpleX partnerei számára. További kiemelőszín híváshiba Csoporttagok letiltása @@ -45,7 +45,7 @@ Az Android Keystore-t a jelmondat biztonságos tárolására használják – lehetővé teszi az értesítési szolgáltatás működését. Hibás az üzenet kivonata Háttér - Megjegyzés: az üzenet- és fájltovábbító kiszolgálók SOCKS proxyn keresztül kapcsolódnak. A hívások és a hivatkozások előnézetének küldése közvetlen kapcsolatot használ.]]> + Megjegyzés: az üzenet- és a fájlátjátszók SOCKS proxyn keresztül kapcsolódnak. A hívások pedig közvetlen kapcsolatot használnak.]]> Alkalmazásadatok biztonsági mentése Az adatbázis előkészítése sikertelen Az összes partnerével továbbra is kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára. @@ -91,7 +91,7 @@ Nem lehet meghívni a partnert! hibás az üzenet azonosítója Partneri kapcsolatkérések automatikus elfogadása - Megjegyzés: NEM fogja tudni helyreállítani, vagy módosítani a jelmondatot abban az esetben, ha elveszíti.]]> + Megjegyzés: NEM fogja tudni helyreállítani vagy módosítani a jelmondatot abban az esetben, ha elveszíti.]]> hívás… További másodlagos szín Hozzáadás egy másik eszközhöz @@ -114,7 +114,7 @@ Az alkalmazásjelkód helyettesítve lesz egy önmegsemmisítő jelkóddal. Arab, bolgár, finn, héber, thai és ukrán – köszönet a felhasználóknak és a Weblate-nek. Engedélyezi a hangüzeneteket? - Mindig legyen használva továbbítókiszolgáló + Mindig legyen használva átjátszó mindig A hívás már véget ért! Engedélyezés @@ -543,7 +543,7 @@ Kiszolgáló megadása kézzel A fájl akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! Hiba történt a csoporthivatkozás létrehozásakor - A galériából + Galéria Engedélyezés (csoport egyéni beállításainak megtartása) Hiba történt a partner törlésekor A tagok véglegesen törölhetik az elküldött üzeneteiket. (24 óra) @@ -618,13 +618,12 @@ Azonnal A fájlok és a médiatartalmak küldése le van tiltva! Profil elrejtése - Hogyan használja a saját kiszolgálóit + Útmutató a saját kiszolgálók használatához Csevegési üzenetek gyorsabb megtalálása Téma importálása Hiba történt a téma importálásakor Partner nevének és az üzenet tartalmának elrejtése Nem kompatibilis adatbázis-verzió - Hogyan működik a SimpleX Nem kompatibilis verzió Elrejtés Bejövő videóhívás @@ -674,7 +673,7 @@ Számítógépek A markdown használata Csevegési profil létrehozása - Védett a kéretlen tartalommal szemben + Védett a kéretlen tartalmakkal szemben Hordozható eszközök leválasztása Különböző nevek, profilképek és átvitelelkülönítés. Elutasítás esetén a kérés küldője NEM kap értesítést. @@ -944,7 +943,7 @@ Biztonsági kód beolvasása a partnere alkalmazásából. Lépjen kapcsolatba a csoport adminisztrátorával. Videó bekapcsolva - Profilnév: + Profil neve: Beillesztés Köszönjük, hogy telepítette a SimpleX Chatet! Csillagozás a GitHubon @@ -979,7 +978,7 @@ %s (jelenlegi) Saját SMP-kiszolgáló Véletlen - Megosztás a partnerekkel + Megosztás a SimpleX partnerekkel Ön Nincsenek csevegései Küldés @@ -995,7 +994,7 @@ Elküldve: %s Jelenlegi profil használata Ez az eszköz - Megosztja a címet a partnereivel? + Megosztja a címet a SimpleX partnereivel? Profiljelszó Téma Eltávolítja a jelmondatot a beállításokból? @@ -1038,7 +1037,7 @@ Kiszolgálók mentése Üdvözlőüzenet mp - A profilfrissítés el lesz küldve a partnerei számára. + A profilfrissítés el lesz küldve a SimpleX partnerei számára. Egyszerűsített inkognitómód Menti az üdvözlőüzenetet? Új csevegési profil létrehozásához indítsa újra az alkalmazást. @@ -1090,7 +1089,7 @@ Menti a beállításokat? Jelkód Ismeretlen hiba - Saját SMP-kiszolgálójának címe + Saját SMP-kiszolgáló címe Csevegési konzol megnyitása Eltávolítás Adatbázis-jelmondat beállítása @@ -1135,7 +1134,7 @@ Elküldve A hangüzenetek küldése le van tiltva. Legutóbbi üzenetek előnézetének megjelenítése - Az előre beállított kiszolgáló címe + Előre beállított kiszolgáló címe Időszakos értesítések letiltva! A jelkód módosult! Akkor fut, amikor az alkalmazás meg van nyitva @@ -1198,7 +1197,7 @@ A fájlok és a médiatartalmak küldése le van tiltva. Fájl megosztása… Mentés - továbbítókiszolgálón keresztül + átjátszón keresztül Megosztás megállítása Ön eltávolította őt: %1$s Jelmondat mentése és a csevegés megnyitása @@ -1435,16 +1434,16 @@ A kézbesítési jelentések le vannak tiltva %d csoportban Néhány nem végzetes hiba történt az importáláskor: Köszönet a felhasználóknak a Weblate-en való közreműködésért! - A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét. + Az átjátszó kiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét. Beállítás a rendszer-hitelesítés helyett. Az üzenetfogadási cím egy másik kiszolgálóra fog módosulni. A cím módosítása akkor fejeződik be, amikor az üzenetküldési kiszolgáló online lesz. A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállításakor nem tud üzeneteket fogadni és küldeni. Jelmondat mentése a Keystore-ba Köszönet a felhasználóknak a Weblate-en való közreműködésért! - Jelmondat mentése a beállításokban + Jelmondat mentése a beállításokba Ennek a csoportnak több mint %1$d tagja van, a kézbesítési jelentések nem lesznek elküldve. A második pipa, ami már nagyon hiányzott! ✅ - A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát. + Az átjátszó kiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát. Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. A mentett WebRTC ICE-kiszolgálók el lesznek távolítva. A kézbesítési jelentések engedélyezve vannak %d csoportban @@ -1510,7 +1509,7 @@ Vagy QR-kód beolvasása Érvénytelen QR-kód Megtartás - Keresés vagy SimpleX-hivatkozás beillesztése + Keressen vagy adjon meg egy SimpleX-hivatkozást Belső hibák megjelenítése Kritikus hiba Belső hiba @@ -1597,7 +1596,7 @@ Feltöltés megerősítése Hiba történt az adatbázis törlésekor Az adminisztrátorok egy tagot a csoport összes tagja számára letilthatnak. - Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-továbbítókiszolgálókra. + Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-átjátszókra. Alkalmazásadatok átköltöztetése Adatbázis archiválása Átköltöztetés visszavonása @@ -1756,7 +1755,7 @@ IP-cím védelme Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról történő letöltések megerősítését (kivéve, ha az .onion vagy a SOCKS proxy engedélyezve van). Ismeretlen kiszolgálók! - Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-továbbítókiszolgálók számára:\n%1$s. + Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-átjátszók számára:\n%1$s. Összes színmód Fekete Színmód @@ -1788,7 +1787,7 @@ További kiemelőszín 2 Alkalmazás témája Perzsa kezelőfelület - Védje az IP-címét a partnerei által kiválasztott üzenetváltási továbbítókiszolgálókkal szemben.\nEngedélyezze a *Hálózat és kiszolgálók* menüben. + Védje az IP-címét a partnerei által kiválasztott üzenetváltási átjátszókkal szemben.\nEngedélyezze a *Hálózat és kiszolgálók* menüben. Ismeretlen kiszolgálókról származó fájlok megerősítése. Továbbfejlesztett üzenetkézbesítés Alkalmazás témájának visszaállítása @@ -1812,7 +1811,7 @@ Fájlkiszolgáló-hiba: %1$s Fájl állapota Fájl állapota: %s - Másolási hiba + Hiba másolása Ezt a hivatkozást egy másik hordozható eszközön már használták, hozzon létre egy új hivatkozást a számítógépén. Ellenőrizze, hogy a hordozható eszköz és a számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint a számítógép tűzfalában engedélyezve van-e a kapcsolat.\nMinden további problémát osszon meg a fejlesztőkkel. Nem lehet üzenetet küldeni @@ -1823,7 +1822,7 @@ Továbbított üzenet Az üzenet később is kézbesíthető, ha a tag aktívvá válik. Még nincs közvetlen kapcsolat, az üzenetet az adminisztrátor továbbítja. - Hivatkozás beolvasása / beillesztése + Hivatkozás megadása vagy QR-kód beolvasása Konfigurált SMP-kiszolgálók Egyéb SMP-kiszolgálók Egyéb XFTP-kiszolgálók @@ -1932,17 +1931,17 @@ Letiltás Letiltva Stabil - Hiba történt a(z) %1$s továbbítókiszolgálóhoz való kapcsolódáskor. Próbálja meg később. - A(z) %1$s célkiszolgáló verziója nem kompatibilis a(z) %2$s továbbítókiszolgálóval. - A(z) %1$s továbbítókiszolgáló nem tudott kapcsolódni a(z) %2$s célkiszolgálóhoz. Próbálja meg később. - A(z) %1$s célkiszolgáló címe nem kompatibilis a(z) %2$s továbbítókiszolgáló beállításaival. + Hiba történt a(z) %1$s továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később. + A(z) %1$s célkiszolgáló verziója nem kompatibilis a(z) %2$s továbbító kiszolgálóval. + A(z) %1$s továbbító kiszolgáló nem tudott kapcsolódni a(z) %2$s célkiszolgálóhoz. Próbálja meg később. + A(z) %1$s célkiszolgáló címe nem kompatibilis a(z) %2$s továbbító kiszolgáló beállításaival. Médiatartalom elhomályosítása Közepes Kikapcsolva Enyhe Erős - A továbbítókiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s. - A továbbítókiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %1$s. + A továbbító kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s. + A továbbító kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %1$s. hívás A partner törölve lesz – ez a művelet nem vonható vissza! Csak a beszélgetés törlése @@ -2002,7 +2001,6 @@ TCP-kapcsolat Mentheti az exportált archívumot. Tippek visszaállítása - Csevegési lista ki/be: Ezt a „Megjelenés” menüben módosíthatja. Új médiabeállítások Lejátszás a csevegési listából. @@ -2097,10 +2095,10 @@ Cím nyilvános megosztása SimpleX-cím megosztása a közösségi médiában. Egyszer használható meghívó megosztása egy baráttal - csak egyetlen partnerrel használható – személyesen vagy bármilyen üzenetváltó-alkalmazáson keresztül megosztható.]]> + csak egyetlen partnerrel használható – személyesen vagy bármilyen üzenetváltó alkalmazáson keresztül megosztható.]]> Beállíthatja a partner nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást. Kapcsolatbiztonság - A SimpleX-cím és az egyszer használható meghívó biztonságosan megosztható bármilyen üzenetváltó-alkalmazáson keresztül. + A SimpleX-cím és az egyszer használható meghívó biztonságosan megosztható bármilyen üzenetváltó alkalmazáson keresztül. A hivatkozás cseréje elleni védelem érdekében összehasonlíthatja a biztonsági kódokat a partnerével. A közösségi médiához Vagy a privát megosztáshoz @@ -2340,10 +2338,9 @@ A tagok összes üzenete meg fog jelenni! moderátorok Elfogadás - A SimpleX Chat használatával Ön elfogadja, hogy:\n- csak elfogadott tartalmakat tesz közzé a nyilvános csoportokban.\n- tiszteletben tartja a többi felhasználót, és nem küld kéretlen tartalmat senkinek. + Ön kijelenti, hogy:\n- nyilvános csoportokban kizárólag megengedett tartalmakat oszt meg\n- tiszteletben tartja a többi felhasználót – nem küld senkinek kéretlen tartalmat Adatvédelmi szabályzat és felhasználási feltételek. - A privát csevegések, a csoportok és a partnerek nem érhetők el a kiszolgálók üzemeltetői számára. - Kiszolgálóüzemeltetők beállítása + Az üzemeltetők kijelentik, hogy:\n- függetlenek maradnak\n- minimálisra csökkentik a metaadatok használatát\n- ellenőrzött, nyílt forráskódú szoftvereket futtatnak Nem támogatott kapcsolattartási hivatkozás Rövid hivatkozás Teljes hivatkozás @@ -2498,10 +2495,10 @@ Tiszta hivatkozás megnyitása Teljes hivatkozás megnyitása Nyomonkövetési paraméterek eltávolítása a hivatkozásokból - SimpleX továbbítókiszolgáló-hivatkozás + SimpleX-átjátszó címe Hiba a csevegés olvasottként való megjelölésekor A célkiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. - A továbbítókiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. + A továbbító kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. nincs feliratkozás Ön nem kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (nincs feliratkozás). @@ -2522,7 +2519,251 @@ Hangüzenetek keresése Videók Hangüzenetek - Nem sikerült létrehozni a kapcsolatot + NEM SIKERÜLT LÉTREHOZNI A KAPCSOLATOT sikertelen - Ha csatornákat hozott létre vagy csatlakozott hozzájuk, akkor azok véglegesen le fognak állni. + Ha csatornákat hozott létre vagy csak csatlakozott hozzájuk, akkor azok véglegesen le fognak állni. + aktív + Közvetítés… + csatorna + Csatorna + Csatorna + Csatornahivatkozás + Csatornatagok + Csatorna neve + Kapcsolódás + kapcsolódott + kapcsolódás + Nyilvános csatorna létrehozása + Nyilvános csatorna létrehozása + Nyilvános csatorna létrehozása (BÉTA) + Csatorna létrehozása + törölve + sikertelen + sikertelen + Hivatkozás + új + meghíva + Csatorna megnyitása + Új csatorna megnyitása + TULAJDONOS + Tulajdonosok + Csatorna elhagyása + Elhagyja a csatornát? + Ellenőrzés + Várakozás + Várakozás a válaszra + Ön + Saját csatorna + Saját csatorna + FELIRATKOZÓ + Feliratkozók + %1$d feliratkozó + %1$d feliratkozó + elfogadva + Csatorna törlése + Törli a csatornát? + Csatlakozás a csatornához + Koppintson a „Csatlakozás a csatornához” gombra + A hangrögzítés nem támogatott az Ön által használt eszközön + Nincsenek engedélyezve csevegési átjátszók. + Kiszolgáló-figyelmeztetés + Ön feliratkozó + Ön nem fog több üzenetet kapni ebből a csatornából. A csevegési előzmények megmaradnak. + átjátszó + A csatorna az összes feliratkozó számára törölve lesz – ez a művelet nem vonható vissza! + A csatorna törölve lesz az Ön számára – ez a művelet nem vonható vissza! + Csatornaprofil szerkesztése + Megoszthat egy hivatkozást vagy egy QR-kódot – bárki képes lesz csatlakozni a csatornához. + Csevegési átjátszók + Eltávolítja a feliratkozót? + A feliratkozó el lesz távolítva a csatornából – ez a művelet nem vonható vissza! + Csevegési átjátszó + Új csevegési átjátszó + Előre beállított átjátszó neve + Előre beállított átjátszó címe + Saját átjátszó neve + Saját átjátszó címe + Adja meg az átjátszó nevét… + Átjátszó használata + Átjátszó tesztelése + Használat új csatornákhoz + Átjátszó törlése + Nem sikerült tesztelni az átjátszót! + Hivatkozás megtekintése + Hivatkozás dekódolása + A teszt a(z) %s. lépésnél sikertelen volt. + A kiszolgáló hitelesítést igényel az átjátszóhoz való kapcsolódáshoz, ellenőrizze a jelszavát. + Érvénytelen az átjátszó neve! + Ellenőrizze az átjátszó nevét, és próbálja újra. + Érvénytelen az átjátszó címe! + Ellenőrizze az átjátszó címét, és próbálja újra. + Hiba az átjátszó hozzáadásakor + Csevegési átjátszók + A csevegési átjátszók továbbítják az üzeneteket az Ön által létrehozott csatornákban. + Csevegési átjátszók + Nincsenek csevegési átjátszók + A csevegési átjátszók továbbítják az üzeneteket a csatorna feliratkozóinak. + %1$d/%2$d átjátszó aktív, %3$d sikertelen + %1$d/%2$d átjátszó aktív + %1$d/%2$d átjátszó kapcsolódva, %3$d hiba + %1$d/%2$d átjátszó kapcsolódva + ÁTJÁTSZÓ + Átjátszóhivatkozás + Átjátszó címe + a következőn keresztül: %1$s + Átjátszó címének megosztása + A feliratkozók az átjátszó hivatkozását használják a csatornához való kapcsolódáshoz.\nAz átjátszó címe ennek az átjátszónak a beállítására szolgált a csatornához. + Ön ezen az átjátszóhivatkozáson keresztül kapcsolódott a csatornához. + Feliratkozó eltávolítása + Az összes feliratkozó számára letiltja a feliratkozót? + Hiba a csatorna létrehozásakor + Visszavonja a csatorna létrehozását? + Engedélyezzen legalább egy csevegési átjátszót a csatorna létrehozásához. + A(z) %1$s nevű profilja meg lesz osztva a csatorna átjátszóival és feliratkozóival.\nAz átjátszók hozzáférhetnek a csatornaüzenetekhez. + Átjátszók konfigurálása + Nem sikerült kapcsolódni az átjátszóhoz + Nem minden átjátszó kapcsolódott + A csatorna %2$d átjátszóból %1$d használatával kezd el működni. Folytatja? + Átjátszó címe + Ez egy csevegési átjátszó címe, nem használható kapcsolódásra. + %1$s nevű csatornához!]]> + Hiba a csatorna megnyitásakor + Az összes feliratkozó számára feloldja a feliratkozó letiltását? + Átjátszó tesztelése a nevének lekéréséhez.]]> + Csatorna teljes neve: + A csatornaprofil a feliratkozók eszközén és a csevegési átjátszókon van tárolva. + csatornaprofil frissítve + %d csatornaesemény + törölt csatorna + hiba: %s + Hiba a csatornaprofil mentésekor + Üzenethiba + Mentés és a csatorna feliratkozóinak értesítése + Csatornaprofil mentése + frissített csatornaprofil + Az alkalmazás %1$d sikertelen letöltési kísérlet után eltávolította ezt az üzenetet. + eltávolítva (%1$d kísérlet) + A csatorna ideiglenesen nem érhető el + A csatornának nincsenek aktív átjátszói. Próbáljon meg később csatlakozni. + nem lehet közvetíteni + az üzemeltető eltávolította + inaktív + Az összes átjátszó el lett távolítva + Nem sikerült kapcsolódni egyetlen átjátszóhoz sem + Nincsenek aktív átjátszók + %1$d átjátszó eltávolítva + %1$d átjátszóhoz nem sikerült kapcsolódni + %1$d átjátszó inaktív + %1$d/%2$d átjátszó aktív, %3$d eltávolítva + %1$d/%2$d átjátszó aktív, %3$d hiba + %1$d/%2$d átjátszó kapcsolódott, %3$d átjátszóhoz nem sikerült kapcsolódni + %1$d/%2$d átjátszó kapcsolódott, %3$d eltávolítva + Az átjátszók hozzáadása később lesz támogatott. + Várakozás a csatorna tulajdonosára az átjátszók hozzáadásához. + Üzleti cím + Csatornahivatkozás + Kapcsolattartási cím + Hiba a csatorna megosztásakor + (a tulajdonostól) + Csoporthivatkozás + Hivatkozás aláírása ellenőrizve. + Egyszer használható meghívó + Csatorna megosztása… + Megosztás egy csevegésen keresztül + ⚠️ Nem sikerült ellenőrizni az aláírást: %s. + (aláírva) + Koppintson ide a megnyitáshoz + Letiltás + Engedélyezés + Engedélyezi a hivatkozások előnézetét? + Hiba + Hálózati hiba + A hivatkozáselőnézet küldése felfedheti az Ön IP-címét a weboldal számára. Ezt később módosíthatja az adatvédelmi beállításokban. + A kapcsolat elérte a kézbesítetlen üzenetek korlátját + Átjátszóeredmények: + Csatornabeállítások + Csak a csatorna tulajdonosai módosíthatják a csatornabeállításokat. + Saját nyilvános cím + Új egyszer használható meghívó + Csatornák + Saját nyilvános cím létrehozása + Hivatkozás vagy QR-kód használata + Egy hivatkozás, ami egyetlen partnerrel való kapcsolat létrehozására szolgál + Saját hivatkozás létrehozása + Bárki számára, aki el szeretné érni Önt + Partner meghívása privátban + Hagyja, hogy valaki elérje Önt + Vagy mutassa meg a QR-kódot személyesen vagy videóhíváson keresztül. + Vagy használja ezt a QR-kódot – nyomtassa ki vagy mutassa meg online. + Küldje el a hivatkozást bármilyen üzenetváltó alkalmazáson keresztül – ez egy biztonságos módszer – és kérje meg a partnerét, hogy illessze be a SimpleX alkalmazásba. + Beszélgessen valakivel + Használja ezt a címet a közösségi oldalakon használt profiljaiban, weboldalakon vagy az e-mail aláírásában. + Könnyebben hívhatja meg a barátait 👋 + Nonprofit irányítás + - Hivatkozások előnézetének küldése.\n- SOCKS proxy használata, ha engedélyezve van.\n- Hiperhivatkozásokon keresztüli adathalászat megakadályozása.\n- Hivatkozások nyomonkövetési paramétereinek eltávolítása. + Tulajdonjog: saját átjátszókat üzemeltethet. + Adatvédelem: tulajdonosok és előfizetők számára. + Nyilvános csatornák – mondja el szabadon a véleményét 🚀 + Megbízhatóság: több átjátszó is használható csatornánként. + Biztonságos webhivatkozások + Biztonság: a csatornák kulcsait a tulajdonosok őrzik. + A SimpleX hálózat hosszú távú működésének biztosítása érdekében. + Az új felhasználók számára egyszerűbbé tettük a kapcsolatok létrehozását. + Fiók nélkül születtünk. + Senki sem követte nyomon a beszélgetéseinket. Senki sem készített térképet arról, hogy merre jártunk. A magánéletünk nem csak egy funkció volt, hanem az életmódunk. + Aztán felléptünk az internetre, és minden platform kért belőlünk egy darabot - nevet, telefonszámot, baráti kapcsolatokat. Elfogadtuk, hogy a kommunikáció ára az, hogy mások megtudják, hogy kivel beszélünk. Minden generáció, az emberek és a technológia is eddig így működött - telefon, e-mail, üzenetküldő programok, közösségi média. Úgy tűnt, ez az egyetlen lehetséges mód. + De van egy másik lehetőség is. Egy hálózat, amelyben nincsenek telefonszámok. Nincsenek felhasználónevek. Nincsenek fiókok. Nincsenek semmiféle felhasználói azonosítók. Egy hálózat, amely összeköti az embereket és titkosított üzeneteket továbbít, anélkül, hogy tudná, ki csatlakozik hozzá. + Nem egy jobb zár mások ajtaján. Nem egy kedvesebb házmester, aki tiszteletben tartja az Ön magánéletét, de mégis nyilvántartást vezet minden látogatójáról. Ön itt nem csak egy vendég. Ön itt otthon van. Nincs az a hatalom, amely beléphetne ide - Ön itt szuverén. + A beszélgetései Önhöz tartoznak, ahogy az internet megjelenése előtt is mindig így volt. A hálózat nem egy hely, amelyet meglátogat. Ez egy olyan hely, amelyet Ön hoz létre saját magának. És senki sem veheti el Öntől, függetlenül attól, hogy privát vagy nyilvános. + A legrégebbi emberi szabadság - beszélgetni az emberekkel, anélkül, hogy mások megfigyelnének - olyan infrastruktúrán alapul, amely nem tudja elárulni. + Mert felszámoltuk a lehetőségét is annak, hogy megtudjuk, Ön kicsoda. Így az önrendelkezése soha nem kerülhet idegen kezekbe. + Legyen szabad a saját hálózatában. + + Feliratkozók jelentései + A közvetlen üzenetek küldése a feliratkozók között engedélyezve van. + A közvetlen üzenetek küldése a feliratkozók között le van tiltva. + Legfeljebb az utolsó 100 üzenet elküldése az új feliratkozók számára. + Az előzmények ne legyenek elküldve az új feliratkozók számára. + A feliratkozók küldhetnek eltűnő üzeneteket. + A feliratkozók küldhetnek egymásnak közvetlen üzeneteket. + A feliratkozók közötti közvetlen üzenetek le vannak tiltva. + A feliratkozók véglegesen törölhetik az elküldött üzeneteiket. (24 óra) + A feliratkozók reakciókat adhatnak hozzá az üzenetekhez. + A feliratkozók küldhetnek hangüzeneteket. + A feliratkozók küldhetnek fájlokat és médiatartalmakat. + A feliratkozók küldhetnek SimpleX-hivatkozásokat. + A feliratkozók jelenthetik az üzeneteket a moderátorok felé. + Legfeljebb az utolsó 100 üzenet lesz elküldve az új feliratkozók számára. + Az előzmények nem lesznek elküldve az új feliratkozók számára. + A csevegés az adminisztrátorokkal engedélyezve van a tagok számára. + A csevegés az adminisztrátorokkal engedélyezve van a feliratkozók számára. + Váljon szabaddá\na saját hálózatában. + A csevegés az adminisztrátorokkal le van tiltva. + A nyilvános csatornákban az adminisztrátorokkal való csevegések nem rendelkeznek végpontok közötti titkosítással – csak megbízható csevegési átjátszókkal használja őket. + A csevegés a tagokkal le van tiltva + Csevegés az adminisztrátorokkal + Engedélyezés + Engedélyezi a csevegést az adminisztrátorokkal? + Profil nevének megadása… + Vágjunk bele + A tagok cseveghetnek az adminisztrátorokkal + nem rendelkeznek végpontok közötti titkosítással. A csevegési átjátszók láthatják ezeket az üzeneteket.]]> + Átköltöztetés + Hálózati kötelezettségvállalások + A hálózati útválasztók nem tudhatják,\nhogy ki kivel beszélget + Nincs fiók. Nincs telefonszám. Nincs e-mail-cím. Nincs személyazonosító.\nA legbiztonságosabb titkosítás. + Az eszközön, nem pedig kiszolgálókon. + Megnyitja a külső hivatkozást? + Privát és biztonságos üzenetváltás. + A csevegés az adminisztrátorokkal le van tiltva. + Értesítések beállítása + Útválasztók beállítása + A feliratkozók cseveghetnek az adminisztrátorokkal + Miért jött létre a SimpleX? + Saját hálózat + Profil létrehozása + Az első hálózat, ahol Ön birtokolja\na saját kapcsolatait és csoportjait. + Alsó sáv + A hivatkozások előnézetét SOCKS proxyn keresztül kéri le a kliens. A DNS-lekérdezés viszont továbbra is történhet helyi szinten, a saját DNS-kiszolgálón keresztül. + Felső sáv diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bigtop_updates_circle_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bigtop_updates_circle_filled.svg new file mode 100644 index 0000000000..c88692fc12 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bigtop_updates_circle_filled.svg @@ -0,0 +1 @@ + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bigtop_updates_padded.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bigtop_updates_padded.svg deleted file mode 100644 index 9f4edcfd98..0000000000 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bigtop_updates_padded.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mobile_3.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mobile_3.svg new file mode 100644 index 0000000000..e731314fcc --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mobile_3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mobile_4.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mobile_4.svg new file mode 100644 index 0000000000..4ed6a064bf --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mobile_4.svg @@ -0,0 +1 @@ + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_qr_code_scanner.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_qr_code_scanner.svg new file mode 100644 index 0000000000..6d012c8956 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_qr_code_scanner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index 2257d93efa..60ed7db384 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -619,7 +619,6 @@ Kesalahan saat menginisialisasi WebView. Pastikan Anda telah menginstal WebView dan arsitektur yang didukung adalah arm64.\nKesalahan: %s Gunakan obrolan Bagaimana caranya - Cara kerja SimpleX Berkala Panggilan suara masuk panggilan suara terenkripsi e2e @@ -2026,7 +2025,6 @@ Platform perpesanan dan aplikasi yang melindungi privasi dan keamanan Anda. Untuk melindungi privasi Anda, SimpleX gunakan ID terpisah untuk setiap kontak. PROXY SOCKS - Alihkan daftar obrolan: Tingkatkan dan buka obrolan Ketuk untuk gabung ke samaran Anda memblokir %s @@ -2345,7 +2343,6 @@ Frasa sandi di Keystore tidak dapat dibaca. Hal ini mungkin terjadi setelah pembaruan sistem yang tidak kompatibel dengan aplikasi. Jika tidak demikian, silakan hubungi pengembang. Terima Dengan menggunakan SimpleX Chat, Anda setuju untuk:\n- hanya mengirim konten legal di grup publik.\n- hormati pengguna lain – tidak ada spam. - Konfigurasikan operator server Kebijakan privasi dan ketentuan penggunaan. Obrolan pribadi, grup, dan kontak Anda tidak dapat diakses oleh operator server. Frasa sandi di Keystore tidak dapat dibaca, silakan masukkan secara manual. Hal ini mungkin terjadi setelah pembaruan sistem yang tidak kompatibel dengan aplikasi. Jika tidak demikian, silakan hubungi pengembang. 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 fe4a658a68..adce58e804 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -270,7 +270,7 @@ Permetti ai tuoi contatti di inviare messaggi vocali. Database della chat eliminato ICONA APP - Ideale per la batteria. Riceverai notifiche solo quando l\'app è in esecuzione (NO servizio in secondo piano).]]> + Ideale per la batteria. Riceverai notifiche solo quando l\'app è in esecuzione (NESSUN servizio in secondo piano).]]> Consuma più batteria! L\'app funziona sempre in secondo piano: le notifiche vengono mostrate istantaneamente.]]> chiamata… annulla anteprima link @@ -299,7 +299,7 @@ Copiato negli appunti Crea link di invito una tantum Crea gruppo segreto - Scansiona codice QR.]]> + Scansiona un codice QR.]]> Dalla Galleria Immagine Video @@ -358,12 +358,11 @@ Videochiamata crittografata e2e terminata Come funziona - Come funziona SimpleX Rispondi alla chiamata Audio spento Audio acceso Chiamate audio e video - Auto-accetta le immagini + Accetta automaticamente le immagini hash del messaggio errato ID messaggio errato Chiamata terminata @@ -525,7 +524,7 @@ Autorizzazione negata! Rifiuta (scansiona o incolla dagli appunti) - Scansiona codice QR + Scansiona un codice QR Inizia una nuova conversazione Tocca il pulsante Grazie per aver installato SimpleX Chat! @@ -1105,11 +1104,11 @@ Indirizzo SimpleX COLORI DELL\'INTERFACCIA I tuoi contatti resteranno connessi. - Aggiungi l\'indirizzo al tuo profilo, in modo che i tuoi contatti possano condividerlo con altre persone. L\'aggiornamento del profilo verrà inviato ai tuoi contatti. + Aggiungi l\'indirizzo al tuo profilo, in modo che i tuoi contatti di SimpleX possano condividerlo con altre persone. L\'aggiornamento del profilo verrà inviato ai tuoi contatti di SimpleX. Crea un indirizzo per consentire alle persone di connettersi con te. Crea indirizzo SimpleX - Condividi con i contatti - Condividere l\'indirizzo con i contatti\? + Condividi con i contatti di SimpleX + Condividere l\'indirizzo con i contatti di SimpleX? Smetti di condividere Inserisci il messaggio di benvenuto… (facoltativo) Ciao! @@ -1144,7 +1143,7 @@ Tema scuro Se non potete incontrarvi di persona, mostra il codice QR in una videochiamata o condividi il link. Parliamo in SimpleX Chat - L\'aggiornamento del profilo verrà inviato ai tuoi contatti. + L\'aggiornamento del profilo verrà inviato ai tuoi contatti di SimpleX. Guida per l\'utente.]]> Menu e avvisi Smettere di condividere l\'indirizzo\? @@ -1362,7 +1361,7 @@ Apri cartella del database La password verrà conservata nelle impostazioni come testo normale dopo averla cambiata o il riavvio dell\'app. La password viene conservata nelle impostazioni come testo normale. - Nota bene: i relay di messaggi e file sono connessi via proxy SOCKS. Le chiamate e l\'invio di anteprime dei link usano una connessione diretta.]]> + Nota bene: i relay di messaggi e file sono connessi via proxy SOCKS. Le chiamate usano una connessione diretta.]]> Cripta i file locali Crittografia di file e media memorizzati Nuova app desktop! @@ -1670,7 +1669,7 @@ Migrazione Migrazione completata Apri la schermata di migrazione - O incolla il link dell\'archivio + O incolla un link dell\'archivio O condividi in modo sicuro questo link del file Incolla link dell\'archivio Chiamate picture-in-picture @@ -1943,7 +1942,7 @@ Riconnetti tutti i server L\'indirizzo del server non è compatibile con le impostazioni di rete: %1$s. File - Scansiona / Incolla link + Incolla link / Scansiona Dimensione carattere Totale inviato Messaggio inoltrato @@ -2041,7 +2040,6 @@ Protegge il tuo indirizzo IP e le connessioni. Salva e riconnetti Ripristina tutti i suggerimenti - Cambia l\'elenco delle chat: Puoi cambiarlo nelle impostazioni dell\'aspetto. Riproduci dall\'elenco delle chat. Aumenta la dimensione dei caratteri. @@ -2207,7 +2205,7 @@ - Apri la chat sul primo messaggio non letto.\n- Salta ai messaggi citati. Condividi indirizzo pubblicamente Condividi l\'indirizzo SimpleX sui social media. - O importa file archivio + O importa un file dell\'archivio Telefoni remoti I messaggi diretti tra i membri sono vietati in questa chat. Dispositivi Xiaomi: attiva l\'avvio automatico nelle impostazioni di sistema per fare funzionare le notifiche.]]> @@ -2376,10 +2374,9 @@ Bloccare i membri per tutti? moderatori Tutti i nuovi messaggi di questi membri verranno nascosti! - Usando SimpleX Chat accetti di:\n- inviare solo contenuto legale nei gruppi pubblici.\n- rispettare gli altri utenti - niente spam. - Le chat private, i gruppi e i tuoi contatti non sono accessibili agli operatori dei server. + Tu ti impegni a:\n- Pubblicare solo contenuto legale nei gruppi pubblici\n- Rispettare gli altri utenti. Niente spam + Gli operatori si impegnano a:\n- Essere indipendenti\n- Minimizzare l\'uso di metadati\n- Eseguire codice open source verificato Accetta - Configura gli operatori dei server Informativa sulla privacy e condizioni d\'uso. Questo link richiede una versione più recente dell\'app. Aggiornala o chiedi al tuo contatto di inviare un link compatibile. Link completo @@ -2534,7 +2531,7 @@ Apri link pulito Apri link completo Rimuovi il tracciamento del link - Link del relay SimpleX + Indirizzo del relay SimpleX Errore nel segnare la lettura L\'impronta digitale nell\'indirizzo del server di destinazione non corrisponde al certificato: %1$s. L\'impronta digitale nell\'indirizzo del server di inoltro non corrisponde al certificato: %1$s. @@ -2558,7 +2555,251 @@ Video Messaggi vocali Filtro - Connessione fallita + CONNESSIONE FALLITA fallito Se sei dentro canali o ne hai creati, essi smetteranno di funzionare definitivamente. + %1$d/%2$d relay attivo/i + %1$d/%2$d relay attivo/i, %3$d fallito/i + %1$d/%2$d relay connesso/i + %1$d/%2$d relay connesso/i, %3$d errori + %1$d iscritto + %1$d iscritti + accettato + attivo + Bloccare l\'iscritto per tutti? + Annullare la creazione del canale? + Prova il relay per recuperare il suo nome.]]> + %1$s!]]> + canale + Canale + Canale + Link del canale + Membri del canale + Nome del canale + Il canale verrà eliminato per tutti gli iscritti, non è reversibile! + Il canale verrà eliminato per te, non è reversibile! + Il canale sarà operativo con %1$d di %2$d relay. Procedere? + Relay di chat + Relay di chat + Relay di chat + Relay di chat + I relay di chat inoltrano i messaggi nei canali che crei. + I relay di chat inoltrano i messaggi agli iscritti del canale. + Controlla l\'indirizzo del relay e riprova. + Controlla il nome del relay e riprova. + Configura i relay + Connetti + connesso + in connessione + Crea canale pubblico + Crea canale pubblico + Crea canale pubblico (BETA) + Creazione canale + Decodifica il link + Elimina canale + Eliminare il canale? + eliminato + Elimina relay + Modifica profilo canale + Attiva almeno un relay di chat per creare un canale. + Inserisci il nome del relay… + Errore di aggiunta del relay + Errore di creazione del canale + Errore di apertura del canale + fallito + fallito + Ottieni link + Indirizzo del relay non valido! + Nome del relay non valido! + invitato + Link + nuovo + Nuovo relay di chat + Nessun relay di chat + Nessun relay di chat attivato. + Non tutti i relay sono connessi + Apri canale + Apri un canale nuovo + PROPRIETARIO + Proprietari + Indirizzo relay preimpostato + Nome relay preimpostato + relay + RELAY + Indirizzo del relay + Indirizzo del relay + Connessione del relay fallita + Link del relay + Prova del relay fallita! + Rimuovi iscritto + Rimuovere l\'iscritto? + Il server richiede l\'autorizzazione per connettersi al relay, controlla la password. + Avviso del server + Condividi l\'indirizzo del relay + ISCRITTO + Iscritti + Gli iscritti usano il link del relay per connettersi al canale.\nL\'indirizzo del relay è stato usato per impostare questo relay per il canale. + L\'iscritto verrà rimosso dal canale, non è reversibile! + Prova fallita al passo %s. + Prova relay + Questo è un indirizzo di relay di chat, non può essere usato per connettersi. + Sbloccare l\'iscritto per tutti? + Usa per canali nuovi + Usa relay + Verifica + via %1$s + La registrazione vocale non è supportata sulla tua piattaforma + Attendi + Attendi risposta + tu + sei iscritto/a + Ti sei connesso/a al canale attraverso questo link del relay. + Il tuo canale + Il tuo canale + Il tuo profilo %1$s verrà condiviso con i relay del canale e gli iscritti.\nI relay hanno accesso ai messaggi del canale. + L\'indirizzo del tuo relay + Il nome del tuo relay + Smetterai di ricevere messaggi da questo canale. La cronologia della chat sarà preservata. + Iscriviti al canale + Esci dal canale + Uscire dal canale? + Tocca Iscriviti al canale + Puoi condividere un link o un codice QR, chiunque sarà in grado di iscriversi al canale. + Trasmetti + Nome completo del canale: + Il profilo del canale è memorizzato sui dispositivi degli iscritti e sui relay di chat. + profilo del canale aggiornato + %d eventi del canale + canale eliminato + scartato (%1$d tentativi) + errore: %s + Errore di salvataggio del profilo del canale + Errore del messaggio + Salva e avvisa gli iscritti del canale + Salva il profilo del canale + L\'app ha rimosso questo messaggio dopo %1$d tentativi di riceverlo. + profilo del canale aggiornato + %1$d/%2$d relay attivi, %3$d errori + %1$d/%2$d relay attivi, %3$d rimossi + %1$d/%2$d relay connessi, %3$d falliti + %1$d/%2$d relay connessi, %3$d rimossi + %1$d relay falliti + %1$d relay non attivi + %1$d relay rimossi + L\'aggiunta di relay verrà supportata prossimamente. + Tutti i relay falliti + Tutti i relay rimossi + impossibile trasmettere + Il canale non ha relay attivi. Prova a iscriverti più tardi. + Canale non disponibile temporaneamente + inattivo + Nessun relay attivo + rimosso da un operatore + In attesa che il proprietario del canale aggiunga dei relay. + Indirizzo di lavoro + Link del canale + Indirizzo di contatto + Errore nella condivisione del canale + (dal proprietario) + Link del gruppo + Firma del link verificata. + Condividi canale… + Condividi via chat + ⚠️ Verifica della firma fallita: %s. + (firmato) + Tocca per aprire + Link una tantum + Disattiva + Attiva + Attivare le anteprime dei link? + Errore + Errore di rete + Risultati relay: + L\'invio di un\'anteprima del link può rivelare il tuo indirizzo IP al sito. Puoi modificarlo nelle impostazioni di Privacy più tardi. + La connessione ha raggiunto il limite di messaggi non consegnati + Preferenze del canale + Solo i proprietari del canale possono modificarne le preferenze. + Un link per una persona da connettere + Canali + Connetti via link o codice QR + Crea il tuo link + Crea il tuo indirizzo pubblico + Per chiunque debba raggiungerti + Invita qualcuno in modo privato + Lascia che qualcuno si connetta a te + Nuovo link una tantum + O mostra il QR di persona o via videochiamata. + O usa questo QR: stampalo o mostralo online. + Invia il link tramite qualsiasi messenger, è sicuro. Chiedi di incollarlo in SimpleX. + Parla con qualcuno + Usa questo indirizzo nel tuo profilo di social media, sito web o firma email. + Il tuo indirizzo pubblico + È più facile invitare i tuoi amici 👋 + Organizzazione non a scopo di lucro + Per la sostenibilità della rete di SimpleX. + - scegli se inviare anteprime dei link.\n- usa il proxy SOCKS se attivato\n- previeni il phishing dei collegamenti ipertestuali.\n- rimuovi il tracciamento dei link. + Proprietà: puoi gestire i tuoi relay personali. + Privacy: per i proprietari e gli iscritti. + Canali pubblici - parla liberamente 🚀 + Affidabilità: relay multipli per canale. + Link web sicuri + Sicurezza: solo i proprietari hanno le chiavi del canale. + Abbiamo semplificato la connessione per i nuovi utenti. + Sei nato senza un account. + Nessuno monitorava le tue conversazioni. Nessuno disegnava una mappa delle tue posizioni. La privacy non era mai stata una caratteristica, era uno stile di vita. + Poi ci siamo trasferiti online e ogni piattaforma ha chiesto un pezzo di noi: il nome, il numero, gli amici. Abbiamo accettato che il prezzo da pagare per comunicare con gli altri fosse quello di far sapere a qualcuno con chi parliamo. Ogni generazione, sia di persone che di tecnologia, ha funzionato così: telefono, email, messenger, social media. Sembrava l\'unico modo possibile. + C\'è un\'altra via. Una rete senza numeri di telefono. Senza nomi utente. Senza account. Senza identificatori utente di alcun tipo. Una rete che connette le persone e trasferisce messaggi crittografati senza sapere chi è connesso. + Non una serratura migliore sulla porta di qualcun altro. Non un padrone di casa più gentile che rispetta la tua privacy, ma che continua a tenere traccia di tutti i visitatori. Non sei un ospite. Sei a casa tua. Nessun re può entrarvi: sei tu il sovrano. + Le tue conversazioni appartengono a te, come è sempre stato prima dell\'avvento di internet. La rete non è un luogo che visiti. È un luogo che crei e possiedi. E nessuno può portartelo via, che tu lo renda privato o pubblico. + La più antica libertà umana, parlare con un\'altra persona senza essere osservati, si basa su un\'infrastruttura che non può tradirla. + Perché abbiamo distrutto il potere di sapere chi sei. In modo che il tuo potere non possa mai esserti sottratto. + Vivi libero nella tua rete. + + Segnalazioni degli iscritti + Permetti l\'invio di messaggi diretti agli iscritti. + Proibisci l\'invio di messaggi diretti agli iscritti. + Invia fino a 100 ultimi messaggi ai nuovi iscritti. + Non inviare la cronologia ai nuovi iscritti. + Gli iscritti al canale possono inviare messaggi a tempo. + Gli iscritti al canale possono inviare messaggi diretti. + I messaggi diretti tra gli iscritti sono vietati. + Gli iscritti al canale possono eliminare irreversibilmente i messaggi inviati. (24 ore) + Gli iscritti al canale possono aggiungere reazioni ai messaggi. + Gli iscritti al canale possono inviare messaggi vocali. + Gli iscritti al canale possono inviare file e contenuti multimediali. + Gli iscritti al canale possono inviare link di Simplex. + Gli iscritti possono segnalare messaggi ai moderatori. + Vengono inviati ai nuovi iscritti fino a 100 ultimi messaggi. + La cronologia non viene inviata ai nuovi iscritti. + Consenti ai membri di chattare con gli amministratori. + Consenti agli iscritti di chattare con gli amministratori. + Vivi libero\nnella tua rete + Le chat con gli amministratori sono vietate. + Le chat con amministratori in canali pubblici non hanno crittografia E2E: usale solo con relay di chat fidati. + Le chat con i membri sono disattivate + Chat con amministratori + Attiva + Attivare le chat con gli amministratori? + Inserisci nome profilo… + I membri possono chattare con gli amministratori. + non sono crittografati end-to-end. I relay di chat possono vedere questi messaggi.]]> + Migra + Impegni sulla rete + Gli instradatori di rete non possono\nsapere chi parla con chi + Nessun account. Nessun telefono. Nessuna email. Nessun identificatore.\nLa crittografia più sicura. + Sul tuo telefono, non sui server. + Aprire il link esterno? + Messaggistica privata e sicura. + Vieta le chat con gli amministratori. + Configura le notifiche + Configura gli instradatori + Gli iscritti possono chattare con gli amministratori. + La prima rete in cui possiedi\ni tuoi contatti e i tuoi gruppi. + Perché costruiamo SimpleX. + La tua rete + Il tuo profilo + Cominciamo + Barra inferiore + L\'anteprima del link verrà richiesta via proxy SOCKS. La ricerca DNS può ancora accadere localmente tramite il tuo risolutore DNS. + Barra superiore 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 fb83b83735..faf69dfd03 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml @@ -482,7 +482,6 @@ הסתר פרופיל איך להשתמש במרקדאון איך זה עובד - איך SimpleX עובדת נתק עזרה הקבוצה תימחק עבור כל חברי הקבוצה – לא ניתן לבטל זאת! @@ -945,7 +944,7 @@ הצג קוד QR נעילת SimpleX כוכב ב־GitHub - לשתף כתובת עם אנשי קשר\? + לשתף כתובת עם אנשי קשר? עצור שיתוף הגדרת קוד גישה נעילת SimpleX @@ -2095,7 +2094,6 @@ לשליחה צ\'אט אחד עם חבר הודעה חדשה - הגדרת מפעילי שרת ניתן להגדיר שרתים דרך הגדרות. אישר אותך ממתין לסקירה 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 4c5b279ba7..5c17946c24 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -153,7 +153,6 @@ 通知の常時受信 SMPサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。 WebRTC ICEサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。 - SimpleX の仕様 通話中 電池消費がより高い!非アクティブ時でもバックグラウンドのサービスが常に稼働します(着信してすぐに通知が出ます)。]]> 発信中 @@ -1097,7 +1096,7 @@ システム認証の代わりに設定します。 プロフィールを非表示にできます! リレー サーバーは IP アドレスを保護しますが、通話時間は監視されます。 - アドレスを連絡先と共有しますか\? + アドレスを連絡先と共有しますか? 保留中の通話 データ移行の確認が正しくない %s を提供しました @@ -1826,7 +1825,6 @@ SMPサーバーの構成 接続中 XFTPサーバーの構成 - チャットリスト表示切り替え 連絡先 メッセージサーバ メディア&ファイルサーバ @@ -2014,7 +2012,6 @@ プライバシーとセキュリティの向上 承諾 プライバシーポリシーと利用条件 - サーバオペレータの設定 承諾 サーバオペレータは、プライベートチャット・グループ・連絡先にはアクセスできません。 SimpleX Chat を利用することで、以下の事項に同意したものと見なされます:\n- パブリックグループでは合法なコンテンツのみを送信すること。\n- 他のユーザを尊重すること、またスパムメッセージを送信しないこと。 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 651d32518f..83f937db32 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml @@ -500,7 +500,6 @@ 설명서 내 서버 사용법 마크다운 사용법 - SimpleX 작동 방식 그룹 초대가 만료되었어요. 그룹 멤버는 보낸 메시지를 영구 삭제할 수 있습니다. (24 시간) 그룹 멤버는 음성 메시지를 보낼 수 있어요. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml index 0ea9328085..92985b15be 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml @@ -215,7 +215,6 @@ Bluetooth Profîla xwe çêke Çawa dişuxule - SimpleX çawa dişuxule Çawa tesîrê li pîlê dike Her serê pêlekê Di cih de 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 1e71459df9..bccd49eed9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml @@ -214,7 +214,6 @@ Ištrinti nuorodą Ištrinti nuorodą\? Klaida kuriant grupės nuorodą - Kaip SimpleX veikia Duomenų bazė šifruota! kūrėjas Ištrinti grupę\? @@ -527,8 +526,7 @@ Nutraukti adreso keitimą Automatiškai priimti kontaktų užklausas Jį įrašius visi duomenys bus pašalinti. - Kiekvienam kontaktui ir grupės nariui bus naudojamas atskiras TCP prisijungimas (ir SOCKS prisijungimo duomenys). -\nTurėkite omenyje: jei turite daug prisijungimų, akumuliatoriaus ir interneto duomenų sąnaudos gali būti žymiai didesnės ir, kartais, prisijungimai gali patirti nesėkmę. + Kiekvienam kontaktui ir grupės nariui bus naudojamas atskiras TCP prisijungimas (ir SOCKS prisijungimo duomenys). \nTurėkite omenyje: jei turite daug prisijungimų, akumuliatoriaus ir interneto duomenų sąnaudos gali būti žymiai didesnės ir, kartais, prisijungimai gali patirti nesėkmę.]]> %1$s nori su jumis susisiekti per Yra įjungtas akumuliatoriaus naudojimo optimizavimas, išjungiantis foninę tarnybą ir periodines užklausas apie naujas žinutes. Nustatymuose galite įjungti ją iš naujo. Visada įjungta diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/lv/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/lv/strings.xml index 2843b2cf8d..c5473aeea4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/lv/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lv/strings.xml @@ -79,9 +79,9 @@ Nederīgs ziņojuma formāts Tiešraide Moderēts apraksts - E2ee Info E2ee - E2ee Info No Pq - E2ee Info Pq + + + Savienojuma lokālais displeja vārds Displeja vārds Savienojums izveidots Apraksts Jūs kopīgojāt vienreizējo saiti inkognito režīmā @@ -266,7 +266,7 @@ Pievienot ziņu Savienoties Vai sūtīt kontakta pieprasījumu? - Sūtot kontakta pieprasījumu, jūs atklāsiet savu SimpleX lietotājvārdu šim kontaktam. Vai vēlaties turpināt? + Sūtīt pieprasījumu bez ziņas Sūtīt pieprasījumu Nevar nosūtīt ziņu @@ -306,7 +306,7 @@ Galerijas attēla poga Galerijas video poga Paldies, ka instalējāt SimpleX - Jūs varat sazināties ar SimpleX čata dibinātāju + Lai sāktu jaunu čatu, palīdzības virsraksts Čata palīdzības pieskāriena poga Saglabāt neizmantoto uzaicinājuma jautājumu @@ -830,7 +830,7 @@ Pārbaudīt savienojumu Pārbaudīt kodu mobilajā ierīcē Šīs ierīces nosaukums - Šīs ierīces versija + Savienots mobilais tālrunis Savienots ar mobilo tālruni Ievadiet šīs ierīces nosaukumu @@ -843,17 +843,17 @@ Atvienot darbvirsmu Atvienot attālo resursdatoru Atvienot attālos resursdatorus - Attālais resursdators tika atvienots (paziņojums) + Attālais resursdators tika atvienots (virsraksts) Attālā vadība tika atvienota (virsraksts) - Attālais resursdators atvienots no + Attālā vadība atvienota ar iemeslu Attālās vadības savienojums apturēts (apraksts) Remote Ctrl savienojums ir pārtraukts Identity Desc Kopēšanas kļūda Vai atvienoties no darbvirsmas? Vienlaicīgi var darboties tikai viena ierīce - Atveriet mobilajā ierīcē un skenējiet QR kodu + Gaida, kad mobilā ierīce pieslēgsies Nepareiza darbvirsmas adrese Nesaderīga darbvirsmas versija @@ -867,7 +867,7 @@ Savienots ar darbvirsmu Savienota darbvirsma Pārbaudiet kodu ar darbvirsmu - Jauna darbvirsma + Saistītās darbvirsmas Datoru Ierīces Saistīto Datoru Iestatījumi @@ -885,10 +885,10 @@ Nejaušs Ports Atvērt Portu Ugunsmūrī Atvērt Portu Ugunsmūrī Apraksts - Attālā Saimniekdatora Kļūda - Trūkst - Attālā Saimniekdatora Kļūda - Nav Aktīvs - Attālā Saimniekdatora Kļūda - Aizņemts - Attālā Saimniekdatora Kļūda - Noildze + + + + Migrate To Device Imports Neizdevās Migrate To Device Atkārtot Importu Migrate To Device Ievadiet Paroli @@ -941,9 +941,9 @@ Tūlītējas paziņojumi Pakalpojumu paziņojumi Pakalpojumu paziņojumi atslēgti - Lai saglabātu privātumu, Simplex izmanto fona pakalpojumu, nevis uznirstošos paziņojumus, tas patērē mazāk datora akumulatora. - To var atslēgt caur iestatījumiem, paziņojumi joprojām tiek rādīti. - Izslēgt akumulatora optimizāciju + + + Izslēdzot pakalpojumu un periodiskos paziņojumus Periodiskie paziņojumi Periodiskie paziņojumi atslēgti @@ -952,15 +952,15 @@ Izslēgt sistēmas ierobežojumu Atslēgt paziņojumus Sistēmas ierobežots fons - Brīdinājums par sistēmas ierobežotu fonu + Sistēmas ierobežots fons zvanā Sistēmas ierobežots fons zvanā - Brīdinājums par sistēmas ierobežotu fonu zvanā + Ievadiet paroli Ievadiet paroli Datu bāzes inicializācijas kļūda Neizdevās inicializēt datu bāzi. - Xiaomi ignorēt akumulatora optimizāciju + Simplex pakalpojumu paziņojums Simplex pakalpojumu paziņojuma teksts Zvana pakalpojumu paziņojums audio zvanam @@ -1240,12 +1240,12 @@ Kamera nav pieejama Atļauja noraidīta Augstāk minētais, tad prievārds turpinājums - Pievienot kontaktu, lai izveidotu saiti vai savienotu, izmantojot saiti - Izveidot grupu, lai izveidotu jaunu grupu + + Lai savienotu, izmantojot saiti Ja esat saņēmis simplex ielūguma saiti, varat to atvērt pārlūkā - Datorā nolasīt QR kodu no lietotnes, izmantojot QR koda nolasīšanu - Mobilajā ierīcē noklikšķiniet uz atvērt mobilajā lietotnē, tad noklikšķiniet uz savienot lietotnē + + Pieņemt savienojuma pieprasījumu? Ja izvēlēsieties noraidīt, sūtītājs netiks informēts Pieņemt kontaktu @@ -1314,9 +1314,9 @@ Jūs tiksiet savienots, kad grupas saimnieka ierīce būs tiešsaistē Jūs tiksiet savienots, kad jūsu savienojuma pieprasījums tiks pieņemts Jūs tiksiet savienots, kad jūsu kontaktu ierīce būs tiešsaistē - Ja jūs nevarat tikties klātienē, rādiet QR videozvanā vai caur citu kanālu + Jūsu čata profils tiks nosūtīts jūsu kontaktam - Ja jūs nevarat tikties klātienē, skenējiet QR videozvanā vai lūdziet ielūguma saiti + Kopīgot ielūguma saiti Ielīmējiet saiti, ko saņēmāt, lai savienotos ar savu kontaktu Uzzināt vairāk @@ -1330,19 +1330,19 @@ Jūs varat kopīgot savu adresi Jūs nezaudēsiet savus kontaktus, ja izdzēsīsiet adresi Kopīgot vienreizēju saiti ar draugu - Vienreizēju saiti var izmantot tikai ar vienu kontaktu + Jūs varat iestatīt savienojuma nosaukumu, lai atcerētos Savienojuma drošība Simplex adrese un vienreizējās saites ir drošas kopīgošanai Lai pasargātu no jūsu saites aizvietošanas, salīdziniet kodus Jūs varat pieņemt vai noraidīt savienojumu - Lasiet vairāk lietotāja rokasgrāmatā ar saiti + Adrese vai vienreizēja saite Savienoties caur saiti Savienoties Ielīmēt Šī virkne nav savienojuma saite - Jūs varat arī savienoties, noklikšķinot uz saites + Jauna saruna Jauns Pievienot kontaktu cilni @@ -1373,13 +1373,13 @@ Tīkla sesijas režīms sesija Tīkla sesijas režīms serveris Tīkla sesijas režīms entitāte - Tīkla sesijas režīms lietotājs. + Tīkla sesijas režīms sesija. Tīkla sesijas režīms serveris. - Tīkla sesijas režīms entitāte. + Atjaunināt tīkla sesijas režīmu? - Atspējot sīpolu viesus, ja nav atbalsta - Socks proxy iestatījumu ierobežojumi + + Tīkla smp proxy režīms privātā maršrutēšana Tīkla smp proxy režīms vienmēr Tīkla smp proxy režīms nezināms @@ -1566,22 +1566,21 @@ Izveidot privātu savienojumu Migrēt no citas ierīces Kā tas darbojas - Kā darbojas SimpleX Lai aizsargātu privātumu, SimpleX izmanto ID rindām Tikai klientu ierīces glabā kontaktu grupas un e2e šifrētas ziņas - Visas ziņas un faili ir e2e šifrēti - Lasiet vairāk GitHub krātuvē. + + Izmantot čatu Ievada paziņojumu režīms Ievada paziņojumu režīma apakšvirsraksts Ievada paziņojumu režīms izslēgts Ievada paziņojumu režīms periodisks Ievada paziņojumu režīms pakalpojums - Ievada paziņojumu režīms izslēgts + Ievada paziņojumu režīms izslēgts, īss apraksts - Ievada paziņojumu režīms periodisks + Ievada paziņojumu režīms periodisks apraksts īsi - Ievada paziņojumu režīms pakalpojums + Ievada paziņojumu režīms pakalpojuma apraksts īsi Ievada paziņojumu režīms akumulators Iestatīt datu bāzes paroli @@ -1591,7 +1590,6 @@ Ievada nosacījumi, izmantojot jūs piekrītat Ievada nosacījumi privātuma politika un lietošanas noteikumi Ievada nosacījumi pieņemt - Ievada nosacījumi konfigurēt servera operatorus Ievada izvēlēties servera operatorus Ievada tīkla operatori Ievada tīkla operatori simplex flux vienošanās @@ -1752,7 +1750,7 @@ Atslēgu glabātuve tiek droši glabāta Iestatījumi tiek glabāti parastā tekstā Šifrēts ar nejaušu frāzi - Nav iespējams atgūt frāzi + Atslēgu glabātuve ļauj saņemt ntfs Frāze tiks saglabāta iestatījumos Jums katru reizi jāievada frāze @@ -1798,7 +1796,6 @@ Apstiprināt datu bāzes jauninājumus Vienas rokas saskarne Sarunu apakšējā josla - Vienas rokas saskarnes karte Vienas rokas saskarnes maiņas instrukcija Terminālis vienmēr redzams Sarunu saraksts vienmēr redzams @@ -1991,8 +1988,8 @@ Operatora nosacījumi pieņemti Operatora nosacījumi pieņemti aktivizētiem operatoriem Jūsu serveri - Operatoru nosacījumi pieņemti - Operatoru nosacījumi tiks pieņemti + + Operators Operatora serveri Operators @@ -2002,17 +1999,17 @@ Operatora izmantošanas slēdzis Izmantot operatora x serverus Operatora nosacījumi neizdevās ielādēt - Operatora nosacījumi pieņemti dažiem - Operatora tie paši nosacījumi tiks piemēroti - Operatora tie paši nosacījumi tiks piemēroti operatoriem - Operatora nosacījumi tiks piemēroti - Operatora nosacījumi tiks pieņemti dažiem - Operatoru nosacījumi arī tiks piemēroti + + + + + + Skatīt nosacījumus Pieņemt nosacījumus Operatora lietošanas nosacījumi Operatora atjaunotie nosacījumi - Operatora, lai izmantotu, pieņemiet nosacījumus + Operatora izmantošana ziņām Operatora izmantošana ziņu saņemšanai Operatora izmantošana ziņu privātai maršrutēšanai @@ -2327,9 +2324,9 @@ Atsauces Atsauces uz jums sarunās. Ziņojumi - Attālinātā hosta kļūda: slikts stāvoklis - Attālinātā hosta kļūda: slikta versija - Attālinātā hosta kļūda: atslēgts + + + Attālinātā kontrole: neaktīva Attālinātā kontrole: slikts stāvoklis Attālinātā kontrole: aizņemta @@ -2341,22 +2338,22 @@ Šī funkcija ir izstrādē. Savienojiet plānu, lai savienotos ar sevi Šis ir jūsu personīgais vienreizējais saite - Jūs jau savienojaties ar %1$s + %1$s]]> Jūs jau savienojaties Jūs jau savienojaties, izmantojot šo vienreizējo saiti Šis ir jūsu personīgais simplex adrese Atkārtot savienojuma pieprasījumu Jūs jau esat pieprasījis savienojumu, izmantojot šo adresi Pievienojieties savai grupai - Šis ir jūsu saite grupai %1$s + %1$s]]> Atkārtot pievienošanās pieprasījumu Grupa jau pastāv Čats jau pastāv - Jūs jau pievienojaties grupai %1$s + %1$s]]> Jūs jau pievienojaties grupai Jūs jau pievienojaties grupai, izmantojot šo saiti - Jūs jau esat grupā %1$s - Jūs jau esat savienots ar %1$s + %1$s]]> + %1$s]]> Savienojiet, izmantojot saiti Aģenta kritiska kļūda Notikusi kritiska kļūda aģentā. @@ -2401,19 +2398,19 @@ Migrēt no ierīces, pabeigt migrāciju Migrēt no ierīces, vai dzēst arhīvu? Migrēt no ierīces, augšupielādētais arhīvs tiks dzēsts - Migrēt no ierīces, izvēlieties migrēt no citas ierīces + Migrēt no ierīces, vai kopīgot šo faila saiti Migrēt no ierīces, dzēst datu bāzi no ierīces Migrēt no ierīces, sarunas uzsākšana vairākās ierīcēs nav atbalstīta Migrēt no ierīces, uzsākt sarunu Migrēt no ierīces, migrācija pabeigta - Migrēt no ierīces, nedrīkstat uzsākt datu bāzi divās ierīcēs - Migrēt no ierīces, izmantošana divās ierīcēs pārtrauc šifrēšanu + + Migrēt no ierīces, pārbaudīt datu bāzes paroli Migrēt no ierīces, pārbaudīt paroli Migrēt no ierīces, apstipriniet, ka atceraties paroli Migrēt no ierīces, pārbaudiet savienojumu un mēģiniet vēlreiz - Migrēt no ierīces, arhīvs tiks dzēsts + Kļūda, migrējot no ierīces, pārbaudot paroli Tīkla veids: nav tīkla savienojuma Tīkla veids: mobilais diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ml/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ml/strings.xml index d21b8b8f83..19aa92a4a0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ml/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ml/strings.xml @@ -353,7 +353,6 @@ സ്വാഗത സന്ദേശം നൽകുക... (ഇച്ഛാനുസൃതമായ) സംരക്ഷിക്കാതെ പുറത്ത് പോവുക ഇത് എങ്ങനെ പ്രവർത്തിക്കുന്നു - SimpleX എങ്ങനെ പ്രവർത്തിക്കുന്നു കൃത്യസമയം പ്രവർത്തനരഹിതമാക്കുക എന്നതിൽ ഇല്ലാതാക്കി 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 655b1cedbd..cc81e5365b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -424,7 +424,6 @@ cursief Hoe het werkt gemiste oproep - Hoe SimpleX werkt Inkomende audio oproep Inkomend video gesprek Negeren @@ -1111,7 +1110,7 @@ Delen met contacten Uw contacten blijven verbonden. Profiel update wordt naar uw contacten verzonden. - Adres delen met contacten\? + Adres delen met contacten? Stop met delen Stop met het delen van adres\? Nodig vrienden uit @@ -2039,7 +2038,6 @@ Verwijder maximaal 20 berichten tegelijk. Sommige bestanden zijn niet geëxporteerd Alle hints resetten - Chat-lijst wisselen: U kunt dit wijzigen in de instellingen onder uiterlijk Creëren Vervagen voor betere privacy. @@ -2380,7 +2378,6 @@ Volledige link Niet-ondersteunde verbindingslink Korte link - Serveroperators configureren Privacybeleid en gebruiksvoorwaarden. Privéchats, groepen en uw contacten zijn niet toegankelijk voor serverbeheerders. contact verwijderd 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 281a734ed3..9cc43851d6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -426,7 +426,6 @@ Możesz używać markdown do formatowania wiadomości: Utwórz swój profil Jak to działa - Jak SimpleX działa Natychmiastowy Jak wpływa na baterię Nawiąż prywatne połączenie @@ -1115,7 +1114,7 @@ Dowiedz się więcej Przestać udostępniać adres\? Automatycznie akceptuj - Udostępnić adres kontaktom\? + Udostępnić adres kontaktom? Wpisz wiadomość powitalną… (opcjonalne) Aktualizacja profilu zostanie wysłana do Twoich kontaktów. Zapisać ustawienia\? @@ -2008,7 +2007,6 @@ Połączenie TCP Nic nie jest zaznaczone Sprawdź czy link SimpleX jest poprawny. - Przełącz listę czatów: Archiwizuj kontakty aby porozmawiać później. Chroni Twój adres IP i połączenia. Osiągalny pasek narzędzi czatu @@ -2230,7 +2228,6 @@ Czat z administratorami Czat z członkiem Czatuj z członkami, zanim dołączą. - Konfigurowanie operatorów serwerów Połącz Połącz się szybciej! 🚀 kontakt usunięty @@ -2557,4 +2554,13 @@ Połączenie nie powiodło się niepowodzenie Jeśli dołączyłeś do kanałów lub je utworzyłeś, przestaną one działać na stałe. + Urodziłeś się bez konta. + Nikt nie śledził twoich rozmów. Nikt nie rysował mapy miejsc, w których byłeś. Prywatność nigdy nie była funkcją - była sposobem na życie. + Następnie przenieśliśmy się do sieci, a każda platforma prosiła o podanie danych osobowych - imienia i nazwiska, numeru telefonu, znajomych. Zaakceptowaliśmy fakt, że ceną za możliwość komunikowania się z innymi jest ujawnienie komuś, z kim rozmawiamy. Tak było w przypadku każdego pokolenia, ludzi i technologii - telefonu, poczty elektronicznej, komunikatorów, mediów społecznościowych. Wydawało się to jedyną możliwą opcją. + Jest jeszcze inny sposób. Sieć bez numerów telefonów. Bez nazw użytkowników. Bez kont. Bez jakichkolwiek tożsamości użytkowników. Sieć, która łączy ludzi i przesyła zaszyfrowane wiadomości, nie wiedząc, kto jest podłączony. + Nie chodzi o lepszy zamek w drzwiach kogoś innego. Nie chodzi o milszego właściciela, który szanuje twoją prywatność, ale nadal prowadzi rejestr wszystkich odwiedzających. Nie jesteś gościem. Jesteś w domu. Żaden król nie może do niego wejść - jesteś suwerenem. + Twoje rozmowy należą do Ciebie, tak jak zawsze było przed pojawieniem się Internetu. Sieć nie jest miejscem, które odwiedzasz. Jest miejscem, które tworzysz i które należy do Ciebie. Nikt nie może Ci tego odebrać, niezależnie od tego, czy jest to miejsce prywatne, czy publiczne. + Najstarsza ludzka wolność - możliwość rozmowy z inną osobą bez bycia obserwowanym - opiera się na infrastrukturze, która nie może jej zdradzić. + Ponieważ zniszczyliśmy moc pozwalającą poznać, kim jesteś. Więc twoja moc nigdy nie będzie Ci odebrana. + Ciesz się swobodą w swojej sieci. 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 c0bbe4d6bb..c129d68521 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 @@ -87,8 +87,7 @@ Aceitar solicitações de contato automaticamente Aparência O serviço em segundo plano está sempre em execução - as notificações serão exibidas assim que as mensagens estiverem disponíveis. - Uma conexão TCP separada (e credencial SOCKS) será usada para cada contato e membro do grupo. -\nAtenção: se você tiver muitas conexões, o consumo de bateria e tráfego pode ser substancialmente maior e algumas conexões podem falhar. + para cada contato e membro do grupo. \nAtenção: se você tiver muitas conexões, o consumo de bateria e tráfego pode ser substancialmente maior e algumas conexões podem falhar.]]> Bom para bateria. O aplicativo procura por mensagens a cada 10 minutos. Você pode perder chamadas ou mensagens urgentes.]]> chamda encerrada %1$s Converse com os desenvolvedores @@ -418,7 +417,6 @@ O arquivo será recebido quando seu contato estiver online, aguarde ou verifique mais tarde! O perfil do grupo é armazenado nos dispositivos dos membros, não nos servidores. ajuda - Como o SimpleX funciona Servidores ICE (um por linha) Ignorar A imagem será recebida quando seu contato estiver online, aguarde ou verifique mais tarde! @@ -1150,7 +1148,7 @@ Salvar configurações de aceitação automática Abrindo banco de dados… Alterar perfis de conversa - Compartilhar endereço com os contatos\? + Compartilhar endereço com os contatos? Seus contatos continuarão conectados. Todos os dados do aplicativo serão excluídos. A senha do aplicativo é substituída por uma senha de auto-destruição. @@ -1872,7 +1870,6 @@ Migrar para outro dispositivo Verificar palavra-passe WiFi - Alternar lista de conversa: Você pode mudar isso em configurações de Aparência. desativado nenhum @@ -2377,7 +2374,6 @@ Os servidores para novos arquivos do seu perfil de chat atual A conexão atingiu o limite de mensagens não entregues, seu contato pode estar offline. Mensagens não entregues - Configurar operadores de servidor Chats privados, grupos e seus contatos não são acessíveis aos operadores de servidor. Aceitar Ao usar o SimpleX Chat, você concorda em:\n- enviar apenas conteúdo legal em grupos públicos.\n- respeitar outros usuários – sem spam. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml index 2f19492237..81cf8ed452 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -525,8 +525,7 @@ Negru Blocați membrul pentru toți? Atât tu, cât și contactul tău puteți șterge definitiv mesajele trimise. (24 de ore) - Folosește mai multă baterie! -\nServiciul în fundal rulează mereu – notificările sunt afișate imediat ce mesajele sunt disponibile. + Folosește mai multă baterie! \nServiciul în fundal rulează mereu – notificările sunt afișate imediat ce mesajele sunt disponibile.]]> Nu se poate trimite mesajul Șterge Confirmați fișiere de la servere necunoscute. @@ -1505,7 +1504,6 @@ Ieșire fără salvare Parolă profil ascuns italic - Cum funcționează SimpleX Fără identificatori de utilizator. Acest lucru se poate întâmpla atunci când:\n1. Mesajele au expirat în aplicația de trimitere după 2 zile sau pe server după 30 de zile.\n2. Decriptarea mesajului a eșuat, deoarece tu sau contactul tău ați folosit un backup vechi al bazei de date.\n3. Conexiunea a fost compromisă. Eroare la exportarea bazei de date a conversației @@ -1892,7 +1890,6 @@ Video Mesaj vocal (%1$s) Trimite - Comută lista de conversații: Pentru a primi notificări, te rugăm să introduci parola bazei de date Se așteaptă videoclipul Mesaje nelivrate @@ -2284,7 +2281,6 @@ Ai partajat o cale de fișier nevalidă. Raportează problema dezvoltatorilor aplicației. Deschide în aplicația mobilă, apoi atinge Conectare în aplicație.]]> Actualizează adresa - Configurați operatorii serverului Condițiile vor fi acceptate pentru operatorii activați după 30 de zile. Apasă pe butonul de informații de lângă bara de adrese pentru a permite accesul la microfon. Colţ 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 5445c57055..b5bbf47973 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -10,10 +10,10 @@ Вы соединитесь со всеми членами группы. Соединиться - соединено + соединен(а) ошибка соединяется - Установлено соединение с сервером, через который Вы получаете сообщения от этого контакта. + Вы подключены к серверу, используемому для приёма сообщений от этого соединения. Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта (ошибка: %1$s). Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта. @@ -27,7 +27,7 @@ connection %1$d соединение установлено - приглашение соединиться + приглашение соединяется… Вы создали одноразовую ссылку Вы создали одноразовую ссылку инкогнито @@ -54,7 +54,7 @@ Превышено время соединения Ошибка соединения - Пожалуйста, проверьте Ваше соединение с сервером %1$s и попробуйте ещё раз. + Пожалуйста, проверьте Ваше соединение с %1$s и попробуйте ещё раз. Ошибка при отправке сообщения Ошибка при добавлении членов группы Ошибка при вступлении в группу @@ -78,22 +78,22 @@ Ошибка теста на шаге %s. Сервер требует авторизации для создания очередей, проверьте пароль. Хэш в адресе сервера не соответствует сертификату. - Соединение + Соединиться Создание очереди Защита очереди Удаление очереди - Разрыв соединения + Отключить Мгновенные уведомления Мгновенные уведомления! Мгновенные уведомления выключены! SimpleX выполняется в фоне вместо уведомлений через сервер.]]> - Он может быть выключен через Настройки – Вы продолжите получать уведомления о сообщениях пока приложение запущено.]]> + Он может быть выключен через Настройки - Вы продолжите получать уведомления о сообщениях пока приложение запущено.]]> Разрешите это в следующем окне, чтобы получать уведомления мгновенно.]]> Оптимизация батареи включена, поэтому сервис уведомлений выключен. Вы можете снова включить его через Настройки. Периодические уведомления Периодические уведомления выключены! - Приложение периодически получает новые сообщения — это потребляет несколько процентов батареи в день. Приложение не использует push уведомления — данные не отправляются с Вашего устройства на сервер. + Приложение периодически получает новые сообщения - это потребляет несколько процентов батареи в день. Приложение не использует push уведомления - данные не отправляются с Вашего устройства на сервер. Введите пароль Для получения уведомлений, пожалуйста, введите пароль от базы данных Ошибка базы данных @@ -114,7 +114,7 @@ Всегда включен Приложение может получить сообщение только тогда, когда запущено, в фоне сервис запускаться не будет Проверять новые сообщения раз в 10 минут на протяжении до 1 минуты - Фоновый сервис всегда включен. Уведомления будут показаны без задержки при наличии сообщений + Фоновый сервис всегда включен. Уведомления будут показаны без задержки при наличии сообщений. Текст сообщения Имена контактов Скрытое @@ -154,10 +154,10 @@ Редактировать Удалить Показать - Спрятать + Скрыть Разрешить Удалить сообщение? - Сообщение будет удалено – это действие нельзя отменить! + Сообщение будет удалено - это действие нельзя отменить! Сообщение будет помечено на удаление. Получатель(и) сможет(смогут) посмотреть это сообщение. Удалить для меня Для всех @@ -203,8 +203,8 @@ Файл Большой файл! - Ваш контакт отправил файл, размер которого превышает поддерживаемый в настоящее время максимальный размер (%1$s). - В настоящее время максимальный поддерживаемый размер файла составляет %1$s. + Ваш контакт отправил файл, размер которого превышает максимальный размер (%1$s). + Максимальный размер файла - %1$s. Ожидается приём файла Файл будет принят, когда Ваш контакт будет в сети, подождите или проверьте позже! Файл сохранён @@ -221,11 +221,11 @@ Контакт и все сообщения будут удалены - это действие нельзя отменить! Удалить контакт Имя контакта… - Соединение с сервером установлено + Соединено Соединение с сервером не установлено - Ошибка соединения с сервером - Ожидается соединение с сервером - Переключить адрес получения? + Ошибка + Ожидает + Поменять адрес получения? Адрес получения сообщений будет перемещён на другой сервер. Изменение адреса завершится после того как отправитель будет онлайн. Отправить сообщение @@ -248,7 +248,7 @@ Начать новый разговор Создать ссылку-приглашение Соединиться через ссылку или QR-код - Сканировать\nQR-код + Сканировать QR-код Создать секретную группу (чтобы отправить Вашему контакту) (сканировать или вставить из буфера) @@ -271,7 +271,7 @@ Сканировать QR-код.]]> Open in mobile app на веб странице, затем нажмите Соединиться в приложении.]]> - Принять запрос на соединение? + Принять запрос? Отправителю НЕ будет послано уведомление, если Вы отклоните запрос на соединение. Принять Принять инкогнито @@ -291,7 +291,7 @@ Без звука Уведомлять - Вы пригласили Ваш контакт + Вы пригласили контакт Вы приняли приглашение соединиться Удалить ожидаемое соединение? Контакт, которому Вы отправили эту ссылку, не сможет соединиться! @@ -311,7 +311,7 @@ удалить превью ссылки Настройки QR-код - SimpleX адрес + Адрес SimpleX помощь SimpleX команда SimpleX логотип @@ -320,14 +320,14 @@ Показать QR-код - Неверный QR-код + Ошибка QR-кода Этот QR-код не является ссылкой! Неверная ссылка! Эта ссылка не является ссылкой-приглашением! - Запрос на соединение послан! + Запрос на соединение отправлен! Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже! Соединение будет установлено, когда Ваш запрос будет принят. Пожалуйста, подождите или проверьте позже! - Соединение будет установлено, когда Ваш контакт будет в сети. Пожалуйста, подождите или проверьте позже. + Соединение будет установлено, когда Ваш контакт будет онлайн. Пожалуйста, подождите или проверьте позже! показать QR-код во время видеозвонка или поделиться ссылкой.]]> Ваш профиль будет отправлен \nВашему контакту @@ -341,8 +341,8 @@ Настройки Ваш адрес SimpleX - База данных - Подробнее о SimpleX Chat + Пароль и экспорт базы + Информация о SimpleX Chat Как использовать Форматирование сообщений Форматирование сообщений @@ -374,9 +374,9 @@ Поставить звёздочку на GitHub Внести свой вклад Оценить приложение - Использовать серверы, предосталенные SimpleX Chat? + Использовать серверы, предоставленные SimpleX Chat? Ваши SMP-серверы - Используются серверы предоставленные SimpleX Chat. + Используются серверы, предоставленные SimpleX Chat. Инфо Как использовать серверы Сохранённые WebRTC ICE-серверы будут удалены. @@ -388,7 +388,7 @@ Сохранить Сеть и серверы Настройки сети - Настройки сети + Дополнительные настройки Использовать SOCKS-прокси? Соединяться с серверами через SOCKS-прокси через порт %d? Прокси должен быть запущен до включения этой опции. Использовать прямое соединение с Интернет? @@ -406,7 +406,7 @@ Создать адрес Удалить адрес? Все контакты, которые соединились через этот адрес, сохранятся. - Поделиться\nссылкой + Поделиться ссылкой Удалить адрес Имя профиля: @@ -444,9 +444,9 @@ Эта строка не является ссылкой-приглашением! Открыть в приложении.]]> - звонок… + входящий звонок… пропущенный звонок - отклоненный звонок + отклонённый звонок принятый звонок звонок соединяется… активный звонок @@ -459,8 +459,8 @@ получен ответ… получено подтверждение… соединяется… - соединено - завершен + соединен(а) + завершён Будущее коммуникаций Более конфиденциальный @@ -473,8 +473,7 @@ Добавьте контакт Как это работает - Как SimpleX работает - Чтобы защитить Вашу конфиденциальность, SimpleX использует разные ID для всех ваших контактов. + Чтобы защитить Вашу конфиденциальность, SimpleX использует разные ID для каждого Вашего контакта. Только пользовательские устройства хранят контакты, группы и сообщения. GitHub репозитория.]]> @@ -491,22 +490,22 @@ e2e зашифрованный аудиозвонок Принять Отклонить - Закрыть - Звонок уже завершен! + Не отвечать + Звонок уже завершён! видеозвонок аудиозвонок Аудио и видеозвонки Ваши звонки - Всегда соединяться через relay + Всегда соединяться через релей Звонки на экране блокировки: - Принимать + Принять Показывать Выключить Ваши ICE-серверы WebRTC ICE-серверы - Relay-сервер защищает Ваш IP-адрес, но может отслеживать продолжительность звонка. - Relay-сервер используется только при необходимости. Другая сторона может видеть Ваш IP-адрес. + Релей-сервер защищает Ваш IP-адрес, но может отслеживать продолжительность звонка. + Релей-сервер используется только при необходимости. Другая сторона может видеть Ваш IP-адрес. Откройте SimpleX Chat\nчтобы принять звонок Вы можете разрешить принимать звонки на экране блокировки через Настройки. @@ -517,7 +516,7 @@ у контакта есть e2e шифрование у контакта нет e2e шифрования peer-to-peer - через relay сервер + через релей-сервер Закончить звонок Выключить видео Включить видео @@ -532,7 +531,7 @@ Отклоненный звонок Соединяющийся звонок Текущий звонок - Звонок завершен + Звонок завершён Принять звонок %1$d пропущенных сообщений @@ -540,10 +539,7 @@ ошибка ID сообщения повторное сообщение Пропущенные сообщения - Это может произойти, когда: -\n1. Клиент отправителя удалил неотправленные сообщения через 2 дня, или сервер – через 30 дней. -\n2. Расшифровка сообщения была невозможна, когда Вы или Ваш контакт использовали старую копию базы данных. -\n3. Соединение компроментировано. + Это может произойти, когда:\n1. Клиент отправителя удалил неотправленные сообщения через 2 дня, или сервер – через 30 дней.\n2. Расшифровка сообщения была невозможна, когда Вы или Ваш контакт использовали старую копию базы данных.\n3. Соединение компроментировано. Конфиденциальность Конфиденциальность @@ -580,7 +576,7 @@ Удалить данные чата Ошибка при запуске чата Остановить чат? - Остановите чат, чтобы экспортировать или импортировать архив чата или удалить базу данных. Вы не сможете получать и отправлять сообщения, пока чат остановлен. + Остановите чат, чтобы экспортировать или импортировать архив чата или удалить данные чата. Вы не сможете получать и отправлять сообщения, пока чат остановлен. Остановить Установите пароль База данных зашифрована случайным паролем. Пожалуйста, поменяйте его перед экспортом. @@ -588,21 +584,21 @@ Ошибка при экспорте архива чата Импортировать архив чата? Текущие данные Вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными. -\nЭто действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. +\nЭто действие нельзя отменить - ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. Импортировать Ошибка при удалении данных чата Ошибка при импорте архива чата Архив чата импортирован Перезапустите приложение, чтобы использовать импортированные данные чата. Удалить профиль? - Это действие нельзя отменить — Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. + Это действие нельзя отменить - Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. Данные чата удалены Перезапустите приложение, чтобы создать новый профиль. Используйте самую последнюю версию архива чата и ТОЛЬКО на одном устройстве, иначе Вы можете перестать получать сообщения от некоторых контактов. Удалить файлы во всех профилях чата Удалить все файлы Удалить файлы и медиа? - Это действие нельзя отменить — все полученные и отправленные файлы будут удалены. Изображения останутся в низком разрешении. + Это действие нельзя отменить - все полученные и отправленные файлы будут удалены. Изображения останутся в низком разрешении. Нет полученных или отправленных файлов %d файл(ов) общим размером %s никогда @@ -612,7 +608,7 @@ %s секунд Удалять сообщения через Включить автоматическое удаление сообщений? - Это действие нельзя отменить — все сообщения, отправленные или полученные раньше чем выбрано, будут удалены. Это может занять несколько минут. + Это действие нельзя отменить - все сообщения, отправленные или полученные раньше чем выбрано, будут удалены. Это может занять несколько минут. Удалить сообщения Ошибка при изменении настройки @@ -623,20 +619,20 @@ Уведомления будут работать только до остановки приложения! Удалить Зашифровать - Поменять + Обновить Текущий пароль… Новый пароль… Подтвердите новый пароль… - Сменить пароль + Поменять пароль Пожалуйста, введите правильный пароль. База данных НЕ зашифрована. Установите пароль, чтобы защитить Ваши данные. Android Keystore используется для безопасного хранения пароля - это позволяет стабильно получать уведомления в фоновом режиме. База данных зашифрована случайным паролем, Вы можете его поменять. Внимание: Вы не сможете восстановить или поменять пароль, если потеряете его.]]> Пароль базы данных будет безопасно сохранён в Android Keystore после запуска чата или изменения пароля - это позволит стабильно получать уведомления. - Пароль не сохранён на устройстве — Вы будете должны ввести его при каждом запуске чата. + Пароль не сохранён на устройстве - Вы будете должны ввести его при каждом запуске чата. Зашифровать базу данных? - Сменить пароль базы данных? + Поменять пароль базы данных? База данных будет зашифрована. База данных будет зашифрована и пароль сохранён в Keystore. Пароль базы данных будет изменён и сохранён в Keystore. @@ -660,7 +656,7 @@ Введите пароль… Сохранить пароль и открыть чат Открыть чат - Попытка изменить пароль базы данных не была завершена. + Попытка поменять пароль базы данных не была завершена. Восстановить резервную копию Восстановить резервную копию? Введите предыдущий пароль после восстановления резервной копии. Это действие нельзя отменить. @@ -680,7 +676,7 @@ Вступление в группу Вы вступили в группу. Устанавливается соединение с пригласившим Вас членом группы. Выйти - Выйти из группы + Выйти из группы? Вы перестанете получать сообщения от этой группы. История чата будет сохранена. Пригласить в группу Группа неактивна @@ -689,7 +685,7 @@ Группа не найдена! Эта группа больше не существует. Нельзя пригласить контакты! - Вы используете профиль инкогнито в этой группе. Для защиты Вашего основного профиля приглашать контакты запрещено. + Вы используете профиль инкогнито в этой группе. Для защиты Вашего основного профиля приглашать контакты запрещено Вы отправили приглашение в группу Вы приглашены в группу @@ -727,7 +723,7 @@ владелец удален(а) - покинул(а) + покинул(а) группу группа удалена приглашен(а) соединяется (представлен(а)) @@ -740,7 +736,7 @@ соединяется Нет контактов для добавления - Роль нового члена группы + Роль члена группы Развернуть выбор роли Пригласить в группу Не приглашать членов @@ -750,7 +746,7 @@ Выбрано контактов: %d Контакты не выбраны Нельзя пригласить контакт! - Вы пытаетесь пригласить контакт, который знает Ваш профиль инкогнито, в группу, где Вы используете основной профиль. + Вы пытаетесь пригласить контакт, который знает Ваш профиль инкогнито, в группу, где Вы используете основной профиль Пригласить в группу %1$s ЧЛЕНОВ ГРУППЫ @@ -765,8 +761,8 @@ Создать ссылку Удалить ссылку? Удалить ссылку - Вы можете поделиться ссылкой или QR-кодом — любой сможет присоединиться к группе. Члены группы останутся, даже если вы позже удалите ссылку. - Все члены группы, которые соединились через эту ссылку, останутся в группе. + Вы можете поделиться ссылкой или QR-кодом - любой сможет присоединиться к группе. Члены группы останутся, даже если вы позже удалите ссылку. + Все члены группы останутся соединены. Ошибка при создании ссылки группы Ошибка при удалении ссылки группы Только владельцы группы могут изменять предпочтения группы. @@ -777,7 +773,7 @@ Удалить члена группы Отправить сообщение - Член группы будет удалён - это действие нельзя отменить. + Член группы будет удалён - это действие нельзя отменить! Удалить ЧЛЕН ГРУППЫ Роль @@ -798,7 +794,7 @@ Получение через Отправка через Состояние сети - Переключить адрес получения + Поменять адрес получения Создать скрытую группу Группа полностью децентрализована – она видна только членам. @@ -818,12 +814,12 @@ Включить TCP keep-alive Сохранить Обновить настройки сети? - Обновление настроек приведёт к переподключению клиента ко всем серверам. + Обновление настроек приведет к сбросу и установке нового соединения со всеми серверами. Обновить Инкогнито Случайный профиль - Режим Инкогнито защищает Вашу конфиденциальность — для каждого контакта создаётся новый случайный профиль. + Режим Инкогнито защищает Вашу конфиденциальность - для каждого контакта создаётся новый случайный профиль. Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя. Когда Вы соединены с контактом инкогнито, тот же самый профиль инкогнито будет использоваться для групп с этим контактом. @@ -847,41 +843,41 @@ Предпочтения контакта Предпочтения группы Предпочтения группы - Настройки чатов + Ваши предпочтения Прямые сообщения Удаление для всех Голосовые сообщения включено включено для Вас включено для контакта - выключено + нет получено, не разрешено Разрешить Вашим контактам необратимо удалять отправленные сообщения. (24 часа) Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам. (24 часа) Контакты могут помечать сообщения для удаления; Вы сможете просмотреть их. Разрешить Вашим контактам отправлять голосовые сообщения. Разрешить голосовые сообщения, только если их разрешает Ваш контакт. - Запретить отправлять голосовые сообщений. + Запретить отправлять голосовые сообщения. Вы и Ваш контакт можете необратимо удалять отправленные сообщения. (24 часа) Только Вы можете необратимо удалять сообщения (Ваш контакт может помечать их на удаление). (24 часа) Только Ваш контакт может необратимо удалять сообщения (Вы можете помечать их на удаление). (24 часа) - Необратимое удаление сообщений запрещено в этой группе. + Необратимое удаление сообщений запрещено. Вы и Ваш контакт можете отправлять голосовые сообщения. Только Вы можете отправлять голосовые сообщения. Только Ваш контакт может отправлять голосовые сообщения. Голосовые сообщения запрещены в этом чате. - Разрешить посылать прямые сообщения членам группы. - Запретить посылать прямые сообщения членам группы. + Разрешить личные сообщения членам группы. + Запретить посылать личные сообщения членам группы. Разрешить необратимо удалять отправленные сообщения. (24 часа) Запретить необратимое удаление сообщений. Разрешить отправлять голосовые сообщения. Запретить отправлять голосовые сообщения. - Члены могут посылать прямые сообщения. + Члены могут посылать личные сообщения. Прямые сообщения между членами группы запрещены. Члены могут необратимо удалять отправленные сообщения. (24 часа) Необратимое удаление сообщений запрещено. - Участники могут отправлять голосовые сообщения. - Голосовые сообщения запрещены. + Члены группы могут отправлять голосовые сообщения. + Голосовые сообщения запрещены в этой группе. Минимальный расход батареи. Вы получите уведомления только когда приложение запущено, без фонового сервиса.]]> Уведомления Когда приложение запущено @@ -899,15 +895,15 @@ %d сек %dс %d мин - %d мес. - %d мес. + %d мес + %d мес %dм %dмес %d час - %d ч. + %d ч %dч %d день - %d нед. + %d нед Исчезающие сообщения Показать код безопасности Подтвердить код безопасности @@ -922,22 +918,22 @@ Не удалось открыть чаты Неправильный код безопасности! Сканировать код - Отправить живое сообщение — оно будет обновляться для получателей по мере того, как Вы его вводите + Отправить живое сообщение - оно будет обновляться для получателей по мере того, как Вы его вводите Создать ссылку группы Запретить отправлять исчезающие сообщения. Исчезающие сообщения запрещены. %dнед %dд - %d нед. + %d недель %d дней - Чтобы подтвердить безопасность end-to-end шифрования с Вашим контактом сравните (или сканируйте) код на ваших устройствах. + Чтобы подтвердить безопасность сквозного шифрования с Вашим контактом сравните (или сканируйте) код на ваших устройствах. %s подтверждён %s не подтверждён Код безопасности Подтвердить Сбросить подтверждение Разрешить исчезающие сообщения, только если Ваш контакт разрешает их Вам. - Запретить посылать исчезающие сообщения. + Запретить отправлять исчезающие сообщения. Члены могут посылать исчезающие сообщения. Что нового Новое в %s @@ -960,7 +956,7 @@ ошибка чата Принять Установить 1 день - неверные данные + ошибка данных Ссылки групп Админы могут создать ссылки для вступления в группу. Автоматически принимать запросы контактов @@ -981,23 +977,23 @@ Только локальные данные профиля Сообщения Серверы для новых соединений Вашего текущего профиля чата - Профили + Ваши профили чата Все чаты и сообщения будут удалены - это нельзя отменить! Сборка приложения: %s Версия приложения: v%s для каждого контакта и члена группы. \nОбратите внимание: если у Вас много контактов, потребление батареи и трафика может быть значительно выше, и некоторые соединения могут не работать.]]> для каждого профиля чата, который Вы имеете в приложении.]]> Версия ядра: v%s - Удалить профиль чата\? + Удалить профиль? Удалить профиль чата для Эта настройка применяется к сообщениям в Вашем текущем профиле чата - Отдельные сессии для + Отдельные транспортные сессии Обновить режим отдельных сессий\? - Имя профиля уже используется + Имя профиля уже используется! Ошибка создания профиля! У Вас уже есть профиль с таким именем. Пожалуйста, выберите другое имя. Ошибка выбора профиля! - По профилю чата или по соединению (БЕТА) + По профилю чата или по соединению (БЕТА). Благодаря пользователям – добавьте переводы через Weblate! Разные имена, аватары и транспортные сессии. Итальянский интерфейс @@ -1012,18 +1008,18 @@ Уменьшенное потребление батареи Отдельные транспортные сессии модерировано - модерировано %s + удалено %s Удалить сообщение члена группы\? Модерировать Сообщение будет удалено для всех членов группы. Сообщение будет помечено как удалённое для всех членов группы. Пожалуйста, свяжитесь с админом группы. - Вы \"читатель\" + только чтение сообщений только чтение сообщений читатель Роль при вступлении Ошибка обновления ссылки группы - Системный + Системная Аудио и видео звонки Ошибка при сохранении пароля пользователя Сохранить серверы\? @@ -1049,19 +1045,19 @@ Приветственное сообщение группы Скрыть профиль Без звука - Сделайте профиль конфиденциальным! + Сделайте профиль скрытым! Дополнительные улучшения скоро! - Теперь админы могут: \n- удалять сообщения членов группы. \n- приостанавливать членов группы (роль наблюдатель) + Теперь админы могут:\n- удалять сообщения членов.\n- приостанавливать членов (роль наблюдатель) Защитите Ваши профили чата паролем! Раскрыть Поддержка bluetooth и другие улучшения. Сохранить приветственное сообщение\? - Установите приветственное сообщение для новых членов группы. - Нажмите на профиль, чтобы переключиться на него. - Благодаря пользователям - добавьте переводы через Weblate! + Установить сообщение для новых членов группы! + Нажмите на профиль, чтобы переключиться. + Благодаря пользователям – добавьте переводы через Weblate! Вы всё равно получите звонки и уведомления в профилях без звука, когда они активные. Вы можете скрыть или отключить уведомления профиля - нажмите и удерживайте профиль, чтобы открыть меню. - Изображение будет принято когда Ваш контакт его загрузит. + Изображение будет принято, когда Ваш контакт его загрузит. Файл будет принят когда Ваш контакт загрузит его. Обновление базы данных Подтвердить обновление базы данных @@ -1129,7 +1125,7 @@ Ошибка расшифровки Блокировка SimpleX не включена! Ошибка хэша сообщения - Хэш предыдущего сообщения отличается. + Хэш предыдущего сообщения отличается Подтвердить код Неправильный код Заблокировать через @@ -1231,8 +1227,8 @@ Запретить реакции на сообщения. секунд ЦВЕТА ИНТЕРФЕЙСА - Поделиться адресом с контактами\? - Обновлённый профиль будет отправлен Вашим контактам. + Поделиться адресом с контактами SimpleX? + Обновление профиля будет отправлено Вашим SimpleX контактам. Об адресе SimpleX Узнать больше Если Вы не можете встретиться лично, покажите QR-код во время видеозвонка или поделитесь ссылкой. @@ -1244,12 +1240,12 @@ Настроить тему Создайте адрес, чтобы можно было соединиться с Вами. Все Ваши контакты сохранятся. Обновлённый профиль будет отправлен Вашим контактам. - Добавьте адрес в свой профиль, чтобы Ваши контакты могли поделиться им. Профиль будет отправлен Вашим контактам. + Добавьте адрес в свой профиль, чтобы Ваши SimpleX контакты могли поделиться им. Профиль будет отправлен Вашим SimpleX контактам. Создать адрес SimpleX - Поделиться с контактами + Поделиться с контактами SimpleX Прекратить делиться адресом\? Автоприём - Введите приветственное сообщение... (по желанию) + Введите приветственное сообщение... (опционально) Сохранить настройки\? Прекратить делиться Продолжить @@ -1311,13 +1307,13 @@ Во время импорта произошли некоторые ошибки: нет текста Поиск - Отключено + Выключено Они могут быть изменены в настройках контактов и групп. Отчёты о доставке выключены! шифрование работает для %s требуется новое соглашение о шифровании для %s Изменение адреса будет прекращено. Будет использоваться старый адрес. - Остановить изменение адреса + Прекратить изменение адреса Контакты Выключить (кроме исключений) шифрование согласовывается… @@ -1330,17 +1326,17 @@ Починить шифрование после восстановления из бэкапа. Починить Нет истории - Отправка отчётов о доставке включена для %d контактов. + Отправка отчётов о доставке включена для %d контактов Отправка отчётов о доставке будет включена для всех контактов во всех видимых профилях чата. Установка для Вашего активного профиля Установки для Вашего активного профиля - Отправка отчётов о доставке выключена для %d контактов. + Отправка отчётов о доставке выключена для %d контактов Шифрование работает, и новое соглашение не требуется. Это может привести к ошибкам соединения! - Вторая галочка, когда сообщение доставлено! ✅ + Вторая галочка - знать, что доставлено! ✅ Вы можете включить их позже в настройках Конфиденциальности. Ошибка при прекращении изменения адреса Прекратить - Остановить изменение адреса? + Прекратить изменение адреса? Не избранный Нотификации перестанут работать, пока вы не перезапустите приложение Таймаут протокола на KB @@ -1365,11 +1361,11 @@ шифрование работает новое соглашение о шифровании разрешено код безопасности изменился - Отправлять отчёты о доставке + Отчёты о доставке %s в %s Починить соединение - Починка не поддерживается контактом. - Восстановление шифрования не поддерживается членом группы + Починка не поддерживается контактом + Починка не поддерживается членом группы Пересогласовать шифрование Быстрый поиск чатов Отчёты о доставке сообщений! @@ -1392,7 +1388,7 @@ Нет отфильтрованных разговоров Пересогласовать Пересогласовать шифрование\? - Запретить посылать файлы и медиа. + Запретить отправлять файлы и медиа. Соединиться Инкогнито Разрешить Открыть настройки приложения @@ -1416,7 +1412,7 @@ %s: %s В этой группе более %1$d членов, отчёты о доставке не отправляются. %s, %s и %d других членов соединены - выключено + выключен Эта функция ещё не поддерживается. Проверьте в следующем релизе. Соединиться напрямую\? Запрос на соединение будет отправлен этому члену группы. @@ -1435,7 +1431,7 @@ Расход батареи приложением / Без ограничений в настройках приложения.]]> База данных будет зашифрована, и пароль сохранён в настройках. Шифруйте сохранённые файлы и медиа - Обратите внимание: соединение с серверами файлов и сообщений устанавливаются через SOCKS-прокси. Звонки и картинки ссылок используют прямое соединение.]]> + Обратите внимание: соединение с серверами файлов и сообщений устанавливаются через SOCKS-прокси. Звонки используют прямое соединение.]]> Шифровать локальные файлы Приложение для компьютера! 6 новых языков интерфейса @@ -1459,12 +1455,12 @@ Пароль хранится в настройках, как открытый текст. Открыть Ошибка при создании контакта - Послать прямое сообщение контакту + Отправить личное сообщение контакту Ошибка отправки приглашения Отправьте сообщение чтобы соединиться запрос на соединение Раскрыть - Блокируйте членов группы + Заблокировать членов группы Повторить запрос на соединение? Ошибка нового соглашения о шифровании удалил(а) контакт @@ -1492,9 +1488,9 @@ Обнаружение по локальной сети и %d других событий Соединиться через ссылку? - Группы инкогнито + Инкогнито группы Вступление в группу уже начато! - %1$d сообщений отмодерировано членом %2$s + %1$d сообщений модерировано членом %2$s %s был отключен]]> Быстрое вступление и надёжная доставка сообщений. Соединиться с самим собой? @@ -1503,7 +1499,7 @@ Компьютер подключен Загрузка файла Подключение к компьютеру - Ошибка нового соглашения о шифровании + Ошибка нового соглашения о шифровании. Компьютеры Исправить имя на %s? Удалить %d сообщений? @@ -1538,7 +1534,7 @@ Неверный путь к файлу Сканируйте с мобильного Отключить компьютер? - Пожалуйста, подождите, пока файл загружается со связанного мобильного устройства. + Пожалуйста, подождите, пока файл загружается со связанного мобильного устройства Все новые сообщения от %s будут скрыты! Версия настольного приложения %s несовместима с этим приложением. заблокировано @@ -1595,15 +1591,15 @@ Поле поиска поддерживает ссылки-приглашения. История сообщений и улучшенный каталог групп. %s неактивен]]> - Превышено максимальное время соединения с компьютером. + Превышено максимальное время соединения с компьютером Компьютер отсоединён Неверный код приглашения у компьютера Загрузка чатов… Включить доступ к камере Нажмите, чтобы сканировать Создать группу: создать новую группу.]]> - Не отправлять историю новым членам группы. - Отправить до 100 последних сообщений новым членам группы. + Не отправлять историю новым членам. + Отправить до 100 последних сообщений новым членам. Все сообщения будут удалены - это нельзя отменить! Камера недоступна Код доступа в приложение @@ -1622,7 +1618,7 @@ Ошибка создания сообщения Ошибка удаления заметки Венгерский и Турецкий интерфейс - Поиск или вставить ссылку SimpleX + Искать или вставить ссылку SimpleX Этот QR-код не является SimpleX-ccылкой. С зашифрованными файлами и медиа. С уменьшенным потреблением батареи. @@ -1633,8 +1629,8 @@ Запустить чат? Личные заметки Доступ к истории - История не отправляется новым членам группы. - До 100 последних сообщений отправляются новым членам группы. + История не отправляется новым членам. + До 100 последних сообщений отправляются новым членам. Показывать внутренние ошибки Ошибка соединения с компьютером %s]]> @@ -1670,7 +1666,7 @@ установлен новый адрес контакта установлена новая картинка профиля профиль обновлён - Версия приложения на компьютере не поддерживается. Пожалуйста, установите одинаковую версию на оба устройства. + Версия приложения на компьютере не поддерживается. Пожалуйста, установите одинаковую версию на оба устройства Внутренняя ошибка Очистить личные заметки? Новый чат @@ -1687,21 +1683,21 @@ %s разблокирован Вы разблокировали %s Разблокировать для всех - Заблокировать члена группы для всех? + Заблокировать для всех? заблокирован - заблокировано админом - Заблокирован админом + заблокировано администратором + Заблокирован администратором Заблокировать для всех Ошибка при блокировании члена группы для всех - Разблокировать члена группы для всех? + Разблокировать члена для всех? Вы заблокировали %s - end-to-end шифрованием с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома.]]> - Чат защищён end-to-end шифрованием. - Чат защищён квантово-устойчивым end-to-end шифрованием. + сквозным шифрованием с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома.]]> + Чат защищён сквозным шифрованием. + Чат защищён квантово-устойчивым сквозным шифрованием. Открыть экран миграции Миграция с другого устройства Установить пароль - стандартное end-to-end шифрование + стандартное сквозное шифрование Приветственное сообщение слишком длинное Сообщение слишком большое Повторить загрузку @@ -1750,7 +1746,7 @@ Завершить миграцию Или передайте эту ссылку Миграция завершена - Внимание: запуск чата на нескольких устройствах не поддерживается и приведёт к сбоям доставки сообщений. + Внимание: запуск чата на нескольких устройствах не поддерживается и приведёт к сбоям доставки сообщений не должны использовать одну и ту же базу данных на двух устройствах.]]> Проверьте подключение к Интернету и повторите попытку Подтвердите, что Вы помните пароль базы данных для её миграции. @@ -1762,9 +1758,9 @@ Видеозвонок Чтобы продолжить, чат должен быть остановлен. Обратите внимание: использование одной и той же базы данных на двух устройствах нарушит расшифровку сообщений от ваших контактов, как свойство защиты соединений.]]> - Внимание: архив будет удален.]]> + Внимание: архив будет удалён.]]> Подтвердите настройки сети - квантово-устойчивым end-to-end шифрованием с идеальной прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома.]]> + квантово-устойчивым сквозным шифрованием с идеальной прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома.]]> Мигрировать сюда Мигрировать на другое устройство Мигрируйте на другое устройство через QR-код. @@ -1774,7 +1770,7 @@ Квантово-устойчивое шифрование Используйте приложение во время звонка. Экспортированный файл не существует - Файл удален или ошибка ссылки + Файл удалён или ошибка ссылки Завершите миграцию на другом устройстве. Выполняется миграция базы данных. \nЭто может занять несколько минут. @@ -1815,7 +1811,7 @@ Ссылки SimpleX Разрешить отправлять ссылки SimpleX. Запретить отправку ссылок SimpleX - Участники могут отправлять ссылки SimpleX. + Члены группы могут отправлять ссылки SimpleX. админы все члены владельцы @@ -1825,7 +1821,7 @@ Включено для Переслать Переслать и сохранить сообщение - Ссылки SimpleX запрещены. + Ссылки SimpleX запрещены в этой группе. Переслать сообщение… Литовский интерфейс Источник сообщения остаётся конфиденциальным. @@ -1836,9 +1832,9 @@ ФАЙЛЫ Новые темы чатов нет - Светлый - Системный - Цвета темного режима + Светлая + Системная + Цвета тёмного режима Получайте файлы безопасно Конфиденциальная доставка 🚀 Улучшенная доставка сообщений @@ -1871,7 +1867,7 @@ Всегда Подтверждать файлы с неизвестных серверов. Всегда использовать конфиденциальную доставку. - Тёмный + Тёмная Отладка доставки Ошибка инициализации WebView. Обновите Вашу систему до новой версии. Свяжитесь с разработчиками. \nОшибка: %s @@ -1880,22 +1876,22 @@ Информация об очереди сообщений Персидский интерфейс Защитить IP-адрес - Защитите ваш IP-адрес от серверов сообщений, выбранных Вашими контактами. \nВключите в настройках Сеть и серверы. - Отправьте сообщения напрямую, когда Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку. + Защитите ваш IP-адрес от серверов сообщений, выбранных Вашими контактами.\nВключите в настройках *Сети и серверов*. + Отправлять сообщения напрямую, когда Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку. Конфиденциальная доставка Использовать конфиденциальную доставку с неизвестными серверами. Использовать конфиденциальную доставку с неизвестными серверами, когда IP-адрес не защищён. Когда IP защищён Да Чтобы защитить Ваш IP-адрес, приложение использует Ваши SMP-серверы для конфиденциальной доставки сообщений. - Изображения профилей + Картинки профилей Все режимы Тема приложения Сбросить на тему приложения Сбросить на тему пользователя Неизвестные серверы! Без Tor или VPN, Ваш IP-адрес будет доступен этим серверам файлов: \n%1$s. - Не использовать конфиденциальную маршрутизацию. + Не использовать конфиденциальную доставку. Никогда Неизвестные серверы Нет @@ -1924,7 +1920,7 @@ Применить к Не удаётся отправить сообщение Бета - Соединeно + Соединено попытки Готово Потвердить удаление контакта? @@ -1945,10 +1941,10 @@ Пересылающий сервер %1$s не смог подключиться к серверу назначения %2$s. Попробуйте позже. Версия пересылающего сервера несовместима с настройками сети: %1$s. Версия сервера назначения %1$s несовместима с пересылающим сервером %2$s. - Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удален. + Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удалён. Выбранные настройки чата запрещают это сообщение. Ошибка файла - Сканировать QR-код/ Вставить ссылку + Вставить ссылку / Сканировать Другие XFTP-серверы Настроенные XFTP-серверы Загрузка %s (%s) @@ -1964,7 +1960,7 @@ Всего Активные соединения Приём сообщений - В ожидании + Ожидает Загружено Статистика серверов будет сброшена - это нельзя отменить! Всего отправлено @@ -2002,7 +1998,7 @@ Подписок игнорировано Скопировать ошибку видеозвонок - Контакт будет удален — это нельзя отменить! + Контакт будет удалён - это нельзя отменить! Оставить разговор Удалить только разговор Удалить без уведомления @@ -2015,7 +2011,7 @@ Показать процент Слабое Среднее - Выключено + Нет Панель приложения внизу Текущий профиль Нет информации, попробуйте перезагрузить @@ -2045,8 +2041,8 @@ Выбрать Сообщения будут удалены для всех членов группы. Сообщения будут помечены как удалённые для всех членов группы. - Контакт удален! - Разговор удален! + Контакт удалён! + Разговор удалён! Член группы неактивен Прямого соединения пока нет, сообщение переслано или будет переслано админом. Ничего не выбрано @@ -2078,7 +2074,7 @@ Пригласить Статус сообщения Контакт соединяется, подождите или проверьте позже! - Контакт удален. + Контакт удалён. Попросите Вашего контакта разрешить звонки. Сохранить и переподключиться Отправьте сообщение, чтобы включить звонки. @@ -2089,14 +2085,14 @@ Ошибки Получено сообщений Сообщений отправлено - Переподключить все подключенные серверы для устранения неполадок доставки сообщений. Это использует дополнительный трафик. + Повторно подключите все серверы, чтобы принудительно доставить сообщения. Используется дополнительный трафик. Переподключить все серверы Переподключить сервер? Переподключить серверы? Сбросить всю статистику Статистика Вы не подключены к этим серверам. Для доставки сообщений на них используется конфиденциальная доставка. - Соединяйтесь с друзьями быстрее + Соединяйтесь с друзьями быстрее. Управляйте своей сетью Защищает ваш IP-адрес и соединения. Открыть настройки серверов @@ -2108,8 +2104,8 @@ Транспортные сессии Состояние соединения и серверов. Удаляйте до 20 сообщений за раз. - Загрузка обновления, не закрывайте приложение. - Файл не найден - скорее всего, файл был удален или отменен. + Загрузка обновления, не закрывайте приложение + Файл не найден - скорее всего, файл был удалён или отменен. Адрес пересылающего сервера несовместим с настройками сети: %1$s. написать Сообщение @@ -2126,13 +2122,12 @@ Новые медиа-опции Пригласить Новое сообщение - Переключите список чатов: Обновление приложения Загружать новые версии из GitHub. Увеличить размер шрифтов. Новый интерфейс 🎉 Открыть из списка чатов. - Сбросить все подсказки. + Сбросить все подсказки Вы можете изменить это в настройках Интерфейса. Пересылка %1$s сообщений Сохранение %1$s сообщений @@ -2155,11 +2150,11 @@ %1$s сообщений не переслано Переслать сообщения… Проверьте правильность ссылки SimpleX. - Неверная ссылка + Ошибка ссылки БАЗА ДАННЫХ - Ошибка инициализации WebView. Убедитесь, что у вас установлен WebView и его поддерживаемая архитектура – arm64.\nОшибка: %s + Ошибка инициализации WebView. Убедитесь, что у вас установлен WebView и его поддерживаемая архитектура - arm64.\nОшибка: %s Звук отключен - Сообщения будут удалены — это нельзя отменить! + Сообщения будут удалены - это нельзя отменить! Ошибка переключения профиля Выберите профиль чата Поделиться профилем @@ -2192,13 +2187,13 @@ Имя пользователя Ваши учётные данные могут быть отправлены в незашифрованном виде. Удалить архив? - Загруженный архив базы данных будет навсегда удален с серверов. + Загруженный архив базы данных будет навсегда удалён с серверов. Принятые условия Принять условия Нет серверов сообщений. Нет серверов для приёма сообщений. Ошибки в настройках серверов. - Для профиля %s: + Для профиля чата %s: Нет серверов файлов и медиа. Нет серверов для приёма файлов. Нет серверов для отправки файлов. @@ -2211,7 +2206,7 @@ Посмотреть условия Посмотреть условия %s.]]> - Условия будут автоматически приняты для включенных операторов: %s + Условия будут автоматически приняты для включенных операторов: %s. Условия приняты: %s. Вебсайт %s.]]> @@ -2251,8 +2246,8 @@ Адрес SimpleX или одноразовая ссылка? Настройки адреса Добавьте сотрудников в разговор. - Бизнес адрес - end-to-end шифрованием, с пост-квантовой безопасностью в прямых разговорах.]]> + Бизнес-адрес + сквозным шифрованием, с пост-квантовой безопасностью в прямых разговорах.]]> Приложение всегда выполняется в фоне Проверять сообщения каждые 10 минут Без фонового сервиса @@ -2272,11 +2267,11 @@ Удалить разговор Удалить разговор? Пригласить в разговор - Разговор будет удален для всех участников - это действие нельзя отменить! + Разговор будет удалён для всех участников - это действие нельзя отменить! Оператор %s серверы %s.]]> - Условия будут приняты: %s + Условия будут приняты: %s. Оператор сети Использовать %s Использовать серверы @@ -2284,15 +2279,15 @@ %s.]]> Или импортировать файл архива Доступная панель чата - Разговор будет удален для Вас - это действие нельзя отменить! + Разговор будет удалён для Вас - это действие нельзя отменить! Покинуть разговор Только владельцы разговора могут поменять предпочтения. Текст условий использования не может быть показан, вы можете посмотреть их через ссылку: Разговор - Участник будет удалён из разговора - это действие нельзя отменить. + Член будет удалён из разговора - это действие нельзя отменить! Серверы по умолчанию Роль будет изменена на %s. Все участники разговора получат уведомление. - Ваш профиль будет отправлен участникам разговора. + Ваш профиль будет отправлен участникам разговора %s.]]> %s.]]> Условия использования @@ -2316,15 +2311,15 @@ Нет серверов для доставки сообщений. Вы можете настроить серверы позже. SimpleX Chat и Flux заключили соглашение добавить серверы под управлением Flux в приложение. - Приложение защищает вашу конфиденциальность, используя разные операторы в каждом разговоре. + Приложение улучшает конфиденциальность, используя разных операторов в каждом разговоре. Когда больше чем один оператор включен, ни один из них не видит метаданные, чтобы определить, кто соединен с кем. Ошибка сохранения серверов Условия будут приняты для включенных операторов через 30 дней. Ошибка приёма условий Соединение достигло предела недоставленных сообщений. Возможно, Ваш контакт не в сети. Чтобы защитить Вашу ссылку от замены, Вы можете сравнить код безопасности. - Например, если ваш контакт получает сообщения через сервер SimpleX Chat, ваше приложение будет доставлять их через сервер Flux. - Прямые сообщения между участниками запрещены в этом разговоре. + Например, если Ваш контакт получает сообщения через сервер SimpleX Chat, Ваше приложение доставит их через сервер Flux. + Прямые сообщения между членами группы запрещены. Группы Удалить Удалить список? @@ -2333,30 +2328,30 @@ Избранное запрошено соединение Редактировать - Предприятия + Бизнесы Включить журналы - О операторах + Об операторах Ошибка при сохранении базы данных Соединение не готово. Ошибка обновления списка чата Ошибка создания списка чатов Список Нет чатов в списке %s. - Без непрочитанных чатов - Никаких чатов + Нет непрочитанных чатов + Нет чатов Чаты не найдены Все чаты будут удалены из списка %s, а сам список удалён Добавить список - Примечания + Заметки Открыть в %s Создать список Добавить в список Изменить список Сохранить список Имя списка... - Исправить соединение? + Починить соединение? Соединение требует повторного согласования шифрования. - Исправление + Починить Выполняется повторное согласование шифрования. принятое приглашение Ошибка при загрузке списков чатов @@ -2365,25 +2360,25 @@ Пожаловаться Спам Пожаловаться на спам: увидят только модераторы группы. - Это действие не может быть отмененено - сообщения, отправленные и полученные в этом чате ранее чем выбранное, будут удалены - Получайте уведомления от упоминаний. + Это действие нельзя отменить - сообщения в этом чате, отправленные или полученные раньше чем выбрано, будут удалены. + Уведомления, когда Вас упомянули. Сообщения о нарушениях запрещены в этой группе. Пожаловаться на нарушение: увидят только модераторы группы. - Установить имя чата… + Имя чата… Улучшенная производительность групп - Приватные названия медиафайлов. + Конфиденциальные названия медиафайлов. Спам Сообщения о нарушениях Непрочитанные упоминания Да Упоминайте членов группы 👋 - Улучшенная приватность и безопасность + Улучшенная конфиденциальность и безопасность Ускорено удаление групп. Ускорена отправка сообщений. Помогайте админам модерировать их группы. Организуйте чаты в списки Вы можете сообщить о нарушениях - Установите время исчезания сообщений в чатах. + Установите срок хранения сообщений в чатах. Вы можете упомянуть до %1$s пользователей в одном сообщении! Причина сообщения? Эта жалоба будет архивирована для вас. @@ -2392,7 +2387,7 @@ Ошибка чтения пароля базы данных сообщение о нарушении заархивировано %s Нарушение правил группы - Неприемлемое сообщение + Неприемлемый контент Другая причина Неприемлемый профиль %d сообщений о нарушениях @@ -2419,7 +2414,7 @@ Не пропустите важные сообщения. Ошибка сохранения настроек заархивированное сообщение о нарушении - архивировать + Архивировать Архивировать сообщение о нарушении? Пароль не может быть прочитан из Keystore. Это могло произойти после обновления системы, несовместимого с приложением. Если это не так, обратитесь к разработчикам. Пароль не может быть прочитан из Keystore, пожалуйста, введите его. Это могло произойти после обновления системы, несовместимого с приложением. Если это не так, обратитесь к разработчикам. @@ -2435,7 +2430,7 @@ Пожаловаться на профиль: увидят только модераторы группы. Сообщения о нарушениях Пожаловаться: увидят только модераторы группы. - Выключить уведомления для всех + Все без звука Использовать TCP-порт %1$s, когда порт не указан. Использовать TCP-порт 443 только для серверов по умолчанию. Все серверы @@ -2460,18 +2455,17 @@ модераторы Удалить членов группы? Принять - Используя SimpleX Chat, Вы согласны:\n- отправлять только законные сообщения в публичных группах.\n- уважать других пользователей – не отправлять спам. - Частные разговоры, группы и Ваши контакты недоступны для операторов серверов. - Настроить операторов серверов + Вы обязуетесь:\n- Только законный контент в публичных группах\n- Уважать других пользователей - без спама + Операторы обязуются:\n- Быть независимыми\n- Минимизировать использование метаданных\n- Использовать проверенный и открытый исходный код Политика конфиденциальности и условия использования. - всех + все Принять Член группы хочет присоединиться. Принять? группа удалена - удален из группы + удалён из группы %d чата(ов) контакт не готов - контакт удален + контакт удалён не синхронизирован запрос на вступление отклонён Новый член группы хочет присоединиться. @@ -2479,16 +2473,16 @@ ожидает одобрения Отклонить Отклонить члена группы? - Ошибка при удалении чата + Ошибка при удалении чата с членом группы Полная ссылка - Ошибка при вступлении члена группы + Ошибка вступления члена группы Ссылка не поддерживается Эта ссылка требует новую версию. Обновите приложение или попросите Ваш контакт прислать совместимую ссылку. %d сообщений Вы можете найти Ваши жалобы в Чате с админами. Чат с админами Чат с членом группы - выключено + нет Одобрять членов группы Чаты с членами группы Приём членов в группу @@ -2499,7 +2493,7 @@ Принять члена группы одобрен админами Жалоба отправлена модераторам - Вы вышли + Вы покинули группу нельзя отправлять %d чатов с членами группы контакт выключен @@ -2509,10 +2503,10 @@ Сохранить настройки вступления? Вы приняли этого члена группы рассмотрение - Установить вступление в группу + Приём членов в группу Удалить чат с членом группы? Удалить разговор - принял %1$s + принят %1$s Чат с админами Вы приняты 1 чат с членом группы @@ -2522,7 +2516,7 @@ Добавить сообщение О себе: Нельзя поменять профиль - end-to-end шифрованием.]]> + сквозным шифрованием.]]> только после того как Ваш запрос будет принят.]]> Чат с админами Общайтесь с членами группы до того как принять их. @@ -2548,8 +2542,8 @@ Таймаут конфиденциальной доставки Адрес будет коротким, и Ваш профиль будет добавлен в адрес. Фоновый таймаут протокола - Отклонить запрос на соединение - Может удалять сообщения и блокировать членов группы. + Отклонить запрос + Может удалять сообщения и блокировать членов. запрос отправлен Одобрять членов группы Отправить запрос на соединение? @@ -2560,7 +2554,7 @@ Обновить ссылку группы? Обновить Обновить адрес? - Цель: + Описание: Фоновый таймаут TCP-соединения Отправитель не будет уведомлён. Член группы удалён - невозможно принять запрос @@ -2572,7 +2566,7 @@ Использовать профиль инкогнито 4 новых языков интерфейса Принять запрос на соединение - Бизнес контакт + Бизнес-контакт Каталонский, Индонезийский, Румынский и Вьетнамский - благодаря нашим пользователям! Создайте Ваш адрес Описание слишком длинное @@ -2592,7 +2586,7 @@ Обновите Ваш адрес Обновить ссылку группы Приветствуйте Ваши контакты 👋 - Ваш бизнес контакт + Ваш бизнес-контакт Ваш контакт Ваша группа Разрешить файлы и медиа, только если их разрешает Ваш контакт. @@ -2604,7 +2598,7 @@ Только Ваш контакт может отправлять файлы и медиа. Откройте чтобы использовать бот Запретить отправлять файлы и медиа. - Нажмите Соединиться, чтобы использовать бот. + Нажмите Соединиться, чтобы использовать бот Вы должны быть соединены, чтобы отправлять команды. Удалённые настройки Открыть очищенную ссылку @@ -2613,15 +2607,15 @@ Ошибка прочтения чата Хэш в адресе пересылающего сервера не соответствует сертификату: %1$s. Хэш в адресе сервера не соответствует сертификату: %1$s. - Ссылка SimpleX relay + Адрес релея SimpleX Хэш в адресе сервера назначения не соответствует сертификату: %1$s. - Удалить сообщения участника - Удалить сообщения участника? + Удалить сообщения члена группы + Удалить сообщения члена группы? Удалить сообщения - Сообщения участника будут удалены - это действие не обратимо! + Сообщения члена группы будут удалены - это нельзя отменить! нет подписки - Вы не подключенны к серверу через который Вы получали сообщения от этого контакта (без подписки). - Удалить члена группы и удалить сообщения + Вы не подключены к серверу, через который Вы получали сообщения от этого контакта (нет подписки). + Удалить вместе с сообщениями Все сообщения Файлы Фильтр @@ -2634,4 +2628,250 @@ Поиск голосовых сообщений Видео Голосовые сообщения + %1$d подписчик + %1$d подписчиков + Отменить создание канала? + Это ваша ссылка на канал %1$s! + канал + Канал + Канал + Полное имя канала: + Ссылка канала + Участники канала + Имя канала + профиль канала обновлён + Канал будет удалён для всех подписчиков - это нельзя отменить! + Канал будет удалён для Вас - это нельзя отменить! + Настроить релеи + Соединиться + соединен(а) + соединяется + Создать публичный канал + Создать публичный канал + Создать публичный канал (БЕТА) + Создание канала + %d событий канала + Удалить канал + Удалить канал? + Удалить релей + Редактировать профиль канала + Введите имя релея… + Ошибка добавления релея + Ошибка при создании канала + Ошибка при открытии канала + ошибка: %s + Ошибка при сохранении профиля канала + Неверный адрес релея! + Неверное имя релея! + приглашен(а) + Войти в канал + Покинуть канал + Выйти из канала? + Ссылка + %1$d/%2$d релеев активны + %1$d/%2$d релеев подключены + %1$d/%2$d релеев подключены, %3$d с ошибками + принят(а) + активный + Заблокировать подписчика для всех? + Опубликовать + Канал начнёт работу с %1$d из %2$d релеев. Продолжить? + Чат-релей + Чат-релеи + Чат-релеи + Чат-релеи + Чат-релеи пересылают сообщения в Ваших каналах. + Чат-релеи пересылают сообщения подписчикам каналов. + Проверьте адрес релея и попробуйте снова. + Проверьте имя релея и попробуйте снова. + удалено + Предпочтения канала + Ссылка канала + Профиль канала хранится на устройствах подписчиков и на чат-релеях. + Канал временно недоступен + Выключить + Включить + Ошибка + Бизнес-адрес + нельзя публиковать + Включите хотя бы один релей чатов для создания канала. + Новый чат-релей + Нет чат-релеев + Чат-релеи не включены. + Это адрес чат-релея, с ним нельзя соединиться. + %1$d/%2$d релеев активны, %3$d с ошибками + %1$d/%2$d релеев активны, %3$d с ошибками + %1$d/%2$d релеев активны, %3$d удалены + %1$d/%2$d релеев подключены, %3$d с ошибками + %1$d/%2$d релеев подключены, %3$d удалены + %1$d релеев не работает + %1$d релеев неактивно + %1$d релеев удалено + Нет активных релеев + У канала нет активных релеев. Попробуйте подключиться позже. + Включить картинки ссылок? + Каналы + удалил(а) канал + Адрес контакта + Все релеи удалены + неактивен + Подпись ссылки проверена. + Открыть канал + Открыть новый канал + Владельцы + ВЛАДЕЛЕЦ + релей + РЕЛЕЙ + Адрес релея + Адрес релея + Ошибка подключения релея + Ссылка релея + Результаты релея: + удалено оператором + Удалить подписчика + Удалить подписчика? + Сохранить и уведомить подписчиков канала + Сохранить профиль канала + Отправка картинки ссылки может раскрыть Ваш IP-адрес веб-сайту. Вы можете изменить это в настройках безопасности позже. + Отправьте ссылку через любой мессенджер - это безопасно. Попросите вставить её в SimpleX. + Для подключения к релею требуется авторизация, проверьте пароль. + Предупреждение сервера + Поделиться каналом… + Поделиться адресом релея + Поделиться в чате + ⚠️ Ошибка проверки подписи: %s. + ПОДПИСЧИК + Подписчики + Подписчик будет удалён из канала - это нельзя отменить! + Начните разговор + Нажмите Войти в канал + Нажмите, чтобы открыть + Вы можете поделиться ссылкой или QR-кодом - любой сможет вступить в канал. + Изменить настройки канала могут только владельцы канала. + Одноразовая ссылка + Вы родились без аккаунта. + Никто не отслеживал ваши разговоры. Никто не составлял карту ваших перемещений. Конфиденциальность не была функцией - это был образ жизни. + Потом мы вышли в интернет, и каждая платформа попросила частичку вас - ваше имя, ваш номер, ваших друзей. Мы смирились с тем, что за возможность общаться приходится отдавать информацию о том, с кем мы общаемся. Каждое поколение людей и технологий жило так - телефон, электронная почта, мессенджеры, социальные сети. Казалось, что другого пути нет. + Другой путь есть. Сеть без номеров телефонов. Без имён пользователей. Без аккаунтов. Без каких-либо идентификаторов пользователей. Сеть, которая соединяет людей и передаёт зашифрованные сообщения, не зная, кто с кем связан. + Не более надёжный замок на чужой двери. Не более вежливый хозяин, который уважает вашу частную жизнь, но всё равно ведёт учёт всех посетителей. Вы не гость. Вы у себя дома. Ни один король не войдёт в ваш дом - вы суверенны. + Ваши разговоры принадлежат вам, как это всегда было до интернета. Сеть - это не место, куда вы приходите. Это место, которое вы создаёте и которым владеете. И никто не может это у вас отнять, делаете ли вы его конфиденциальным или публичным. + Древнейшая человеческая свобода - говорить с другим человеком без слежки - построенная на инфраструктуре, которая не может её предать. + Потому что мы разрушили саму возможность узнать, кто вы. Чтобы вашу свободу невозможно было отнять. + Будь свободен в своей сети. + Запись голосовых сообщений не поддерживается на вашей платформе + не защищены сквозным шифрованием. Чат-релеи могут видеть эти сообщения.]]> + Дайте собеседнику Вашу ссылку + Соединитесь по ссылке или QR + Создайте Вашу ссылку + Пригласите конфиденциально + Ссылка для одного человека + Создайте Ваш публичный адрес + Ваш публичный адрес + Любой может связаться с Вами + Ваш канал + Ссылка группы + (от владельца) + (с подписью) + Ошибка при публикации канала + Вы подписчик + Новая одноразовая ссылка + Или покажите QR лично или через видеозвонок. + Используйте этот адрес в профиле социальных сетей, на сайте или в подписи email. + Или используйте этот QR - распечатайте или покажите онлайн. + Будь свободен\nв своей сети + Конфиденциальный и безопасный обмен сообщениями. + Первая сеть, в которой Вы владеете\nсвоими контактами и группами. + Начать + Зачем создан SimpleX. + Ваш профиль + На Вашем телефоне, не на серверах. + Без аккаунта. Без номера. Без email. Без ID.\nСамое безопасное шифрование. + Введите имя профиля... + Мигрировать + Ваша сеть + Серверы сети не могут знать,\nкто с кем общается + Настроить серверы + Настроить уведомления + Обязательства сети + Открыть внешнюю ссылку? + удалено (%1$d попыток) + Ошибка сообщения + Приложение удалило это сообщение после %1$d попыток его получить. + Если Вы присоединились к каналам или создали их, они перестанут работать навсегда. + Вы перестанете получать сообщения из этого канала. История чата сохранится. + обновлён профиль канала + ошибка + ОШИБКА СОЕДИНЕНИЯ + Чат с админами + Разрешить членам группы общаться с админами. + Запретить чаты с админами. + Члены группы могут общаться с админами. + Чаты с админами запрещены. + Чаты с админами в публичных каналах не имеют E2E шифрования - используйте только с доверенными чат-релеями. + Включить чаты с админами? + Включить + Сообщения о нарушениях + Разрешить отправку личных сообщений подписчикам. + Запретить отправку личных сообщений подписчикам. + Отправлять до 100 последних сообщений новым подписчикам. + Не отправлять историю новым подписчикам. + Подписчики могут отправлять исчезающие сообщения. + Подписчики могут отправлять личные сообщения. + Прямые сообщения между подписчиками запрещены. + Подписчики могут необратимо удалять отправленные сообщения. (24 часа) + Подписчики могут добавлять реакции на сообщения. + Подписчики могут отправлять голосовые сообщения. + Подписчики могут отправлять файлы и медиа. + Подписчики могут отправлять ссылки SimpleX. + Подписчики могут отправлять сообщения о нарушениях модераторам. + До 100 последних сообщений отправляется новым подписчикам. + История не отправляется новым подписчикам. + Разрешить подписчикам общаться с админами. + Подписчики могут общаться с админами. + Чаты с членами группы отключены + Публичные каналы - говорите свободно 🚀 + Надёжность: несколько релеев на каждый канал. + Владение: Вы можете запустить свои собственные релеи. + Безопасность: владельцы хранят ключи канала. + Конфиденциальность: для владельцев и подписчиков. + Проще пригласить друзей 👋 + Мы упростили подключение для новых пользователей. + Безопасные веб-ссылки + - включение картинок ссылок.\n- использовать SOCKS-прокси, если включен\n- защита от фишинга.\n- удаление трекинга ссылок. + Некоммерческое управление + Чтобы сохранить сеть SimpleX для всех. + Вы + Имя релея по умолчанию + Адрес релея по умолчанию + Ваше имя релея + Ваш адрес релея + Использовать релей + Тест релея + Использовать для новых каналов + Протестируйте релей, чтобы получить его имя.]]> + Тест релея не пройден! + Получить ссылку + Расшифровать ссылку + Ожидание ответа + Проверить + Ошибка теста на шаге %s. + ошибка + новый + Все релеи недоступны + Добавление релеев будет поддерживаться позже. + Ожидает, когда владелец канала добавит релеи. + через %1$s + Подписчики используют ссылку релея для подключения к каналу.\nАдрес релея был использован для настройки этого релея для канала. + Вы подключились к каналу через эту ссылку релея. + Соединение достигло лимита недоставленных сообщений + Ошибка сети + Ваш профиль %1$s будет отправлен чат-релеям и подписчикам канала.\nРелеи могут видеть сообщения канала. + ошибка + Не все релеи подключены + Подождать + Ваш канал + Разблокировать подписчика для всех? + Нижнее меню + Картинка ссылки будет загружена через SOCKS-прокси. DNS-запрос может быть локальным через Ваш резолвер. + Верхнее меню 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 411cbde4c4..c355d8d9fb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml @@ -376,7 +376,6 @@ วิธีใช้มาร์กดาวน์ สิ้นสุดลงแล้ว มันทำงานอย่างไร - วิธีการ SimpleX ทํางานอย่างไร กระจายอำนาจแล้ว การโทรเสียงแบบ encrypted จากต้นจนจบ การโทรวิดีแบบ encrypted จากต้นจนจบ @@ -909,7 +908,7 @@ แสดง: แสดงตัวเลือกสําหรับนักพัฒนาซอฟต์แวร์ แชร์ลิงก์ - แชร์ที่อยู่กับผู้ติดต่อ\? + แชร์ที่อยู่กับผู้ติดต่อ? แชร์กับผู้ติดต่อ บันทึกการตั้งค่า\? แสดง @@ -1328,4 +1327,4 @@ ในการตอบกลับถึง ไม่มีประวัติ encryptionใช้ได้ - + \ 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 16d821637b..0e9c54fb87 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -611,7 +611,6 @@ Gizle Konuşulan kişileri ve mesajları gizle Uygulamayı, son kullanılanlar kısmından gizle. - SimpleX nasıl çalışıyor 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.]]> bir görüntülü aramada karşıdakinin karekodunu okutabilirsin ya da konuştuğun kişi seninle bir katılım bağlantısı paylaşabilir.]]> 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. @@ -2055,7 +2054,6 @@ Sunucu istatistikleri sıfırlanacaktır - bu geri alınamaz! Erişilebilir uygulama araç çubukları Görünüm ayarlarından değiştirebilirsiniz. - Sohbet listesini değiştir: Sistem modu Erişilebilir sohbet araç çubuğu İçin bilgi gösteriliyor @@ -2270,7 +2268,6 @@ Arkaplan servisi yok Kabul Et SimpleX Chat\'i kullanarak şunları kabul etmiş olursunuz:\n- genel gruplarda sadece yasal içerik göndermeyi.\n- diğer kullanıcılara saygı göstermeyi - spam yapmamayı. - Sunucu operatörlerini yapılandırma Sohbetten çıkılsın mı? yönetici Bütün sunucular 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 6d498ef4ed..4e62631dbb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -407,7 +407,6 @@ очікування підтвердження… Приватність перевизначена Ви вирішуєте, хто може під\'єднатися. - Як працює SimpleX зашифрований e2e аудіовиклик Відкрийте SimpleX Chat для прийняття виклику e2e зашифровано @@ -1887,7 +1886,6 @@ Нове повідомлення Створити Запросити - Перемикнути список чатів: Ви можете змінити це в налаштуваннях зовнішнього вигляду. Статус повідомлення Архівувати контакти, щоб поговорити пізніше. @@ -2376,7 +2374,6 @@ Приватні чати, групи та ваші контакти недоступні для операторів сервера. Прийняти Використовуючи SimpleX Chat, ви погоджуєтесь на:\n- надсилати тільки легальний контент у публічних групах.\n- поважати інших користувачів – без спаму. - Налаштувати операторів сервера Політика конфіденційності та умови використання Це посилання вимагає новішої версії додатку. Будь ласка, оновіть додаток або попросіть вашого контакту надіслати сумісне посилання. Повне посилання diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml index ae22c40277..235158585d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -880,7 +880,6 @@ Hồ sơ trò chuyện ẩn Cách sử dụng Cách thức hoạt động - Cách thức SimpleX hoạt động Cách làm Lỗi khởi động WebView. Hãy đảm bảo bạn đã cài đặt WebView và kiến trúc hỗ trợ của nó là arm64.\nLỗi: %s giờ @@ -1983,7 +1982,6 @@ Tin nhắn này đã bị xóa hoặc vẫn chưa được nhận. Mã QR này không phải là một đường dẫn! Đường dẫn này không phải là một đường dẫn kết nối hợp lệ! - Chuyển đổi danh sách trò chuyện: Thời gian chờ đã hết trong khi kết nối tới máy tính Để cho phép một ứng dụng di động kết nối tới máy tính, mở cổng này trong tường lửa của bạn, nếu bạn có bật nó lên Để bảo vệ sự riêng tư của bạn, SimpleX sử dụng các ID riêng biệt cho mỗi liên hệ bạn có. @@ -2351,7 +2349,6 @@ Bằng việc sử dụng SimpleX Chat, bạn đồng ý:\n- chỉ gửi nội dung hợp pháp trong các nhóm công khai.\n- tôn trọng những người dùng khác - không gửi tin rác. Các cuộc trò chuyện riêng tư, nhóm và liên hệ của bạn không thể truy cập được đối với các bên vận hành máy chủ. Chấp nhận - Định cấu hình các bên vận hành máy chủ Đường dẫn này yêu cầu một phiên bản ứng dụng mới hơn. Vui lòng nâng cấp ứng dụng hoặc yêu cầu liên hệ của một gửi cho một đường dẫn tương thích. Đường dẫn kênh SimpleX Đường dẫn kết nối không được hỗ trợ 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 a67f00a459..1392d7b42b 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 @@ -618,7 +618,6 @@ %d 小时 %d 月 %d 秒 - SimpleX 是如何工作的 确保 WebRTC ICE 服务器地址格式正确、每行分开且不重复。 确保 SMP 服务器地址格式正确、每行分开且不重复。 Markdown 帮助 @@ -1207,7 +1206,7 @@ 消息回应 该聊天禁用了消息回应。 如果你在打开应用程序时输入自毁密码: - 个人资料更新将被发送给你的联系人。 + 个人资料更新将发送给你的联系人。 记录更新于 禁止消息回应。 已收到于 @@ -1350,7 +1349,7 @@ 为所有人启用 需要重新协商加密 已关闭送达回执! - 请注意:消息和文件中继通过 SOCKS 代理连接。呼叫和发送链接预览使用直接连接。]]> + 请注意:消息和文件中继通过 SOCKS 代理连接。通话使用直接连接。]]> 加密本地文件 为存储的文件和媒体加密 全新桌面应用! @@ -1848,7 +1847,7 @@ 尚无直接连接,消息由管理员转发。 其他 SMP 服务器 其他 XFTP 服务器 - 扫描/粘贴链接 + 粘贴链接/扫描 显示百分比 不活跃 缩放 @@ -2012,7 +2011,6 @@ 连接和服务器状态。 最多同时删除 20 条消息 它保护你的 IP 地址和连接。 - 切换聊天列表: 你可以在“外观”设置中更改它。 保存并重新连接 TCP 连接 @@ -2208,12 +2206,12 @@ 离开聊天? 你将停止从这个聊天收到消息。聊天历史将被保留。 邀请加入聊天 - 将为你删除聊天 - 此操作无法撤销! + 将为你删除聊天 —— 此操作无法撤销! 删除聊天 删除聊天? 添加好友 添加团队成员 - 将为所有成员删除聊天 - 此操作无法撤销! + 将为所有成员删除聊天 —— 此操作无法撤销! 仅聊天所有人可更改首选项。 角色将被更改为 %s。聊天中的每个人都会收到通知。 成员之间的私信被禁止。 @@ -2362,9 +2360,8 @@ 将从聊天中移除这些成员 — 此操作无法撤销! 隐私政策和使用条款。 接受 - 使用 SimpleX Chat 代表您同意:\n- 在公开群中只发送合法内容\n- 尊重其他用户 – 没有垃圾信息。 - 服务器运营方无法访问私密聊天、群和你的联系人。 - 配置服务器运营方 + 您承诺:\n- 在公开群中只发送合法内容\n- 尊重其他用户—无垃圾信息 + 运营者承诺:\n- 独立\n- 最小化元数据使用\n- 运行经验证的开源代码 不支持的连接链接 SimpleX 频道链接 短链接 @@ -2518,7 +2515,7 @@ 打开干净链接 打开完整链接 删除链接跟踪 - SimpleX 中继链接 + SimpleX 中继地址 标记为已读时出错 目标服务器地址的指纹和证书不匹配:%1$s。 转发服务器地址的指纹和证书不匹配:%1$s。 @@ -2545,4 +2542,247 @@ 连接失败 失败 如果你加入了或创建了频道,它们会永远停止工作。 + 订阅者使用中继链接连接到频道。\n中继地址用于为频道设置这个中继。 + %1$d 个中继活跃,共 %2$d 个 + %1$d 个中继活跃,共 %2$d 个,%3$d 个失灵 + %1$d 个中继已连接,共 %2$d 个 + %1$d 个中继已连接,共 %2$d 个,%3$d 个出错 + %1$d 位订阅者 + %1$d 位订阅者 + 已接受 + 活跃 + 为所有人拦截订阅者? + 广播 + 取消创建频道? + 测试中继 来获取其名称。]]> + %1$s 频道的链接!]]> + 频道 + 频道 + 频道 + 频道链接 + 频道成员 + 频道名 + 将为所有订阅者删除频道 —— 此操作无法撤销! + 将为你删除频道 —— 此操作无法撤销! + 频道将开始用 %2$d 个中继中的 %1$d 个中继运作。要继续吗? + 聊天中继 + 聊天中继 + 聊天中继 + 聊天中继 + 聊天中继在你创建的频道中转发消息。 + 聊天中继转发消息给频道订阅者。 + 检查中继地址并重试。 + 检查中继名并重试。 + 配置中继 + 连接 + 已连接 + 正在连接 + 创建公开频道 + 创建公开频道 + 创建公开频道(测试版) + 正在创建频道 + 解码链接 + 删除频道 + 删除频道吗? + 已删除 + 删除中继 + 编辑频道简介 + 要创建频道至少启用一个聊天中继。 + 输入中继名… + 添加中继出错 + 创建频道出错 + 打开频道出错 + 失败 + 失败 + 获取链接 + 无效的中继地址! + 无效的中继名! + 已邀请 + 加入频道 + 离开频道 + 离开频道? + 链接 + + 新聊天中继 + 无聊天中继 + 未启用聊天中继。 + 不是所有中继均已连接 + 打开频道 + 打开新频道 + 所有者 + 所有者 + 预设中继地址 + 预设中继名 + 中继 + 中继 + 中继地址 + 中继地址 + 中继连接失败 + 中继链接 + 中继测试失败! + 删除订阅者 + 删除订阅者? + 服务器需要身份认证来连接到中继,检查密码。 + 服务器警告 + 分享中继地址 + 订阅者 + 订阅者 + 将从频道删除订阅者 —— 此操作无法撤销! + 轻触加入频道 + 测试在第 %s 步失败。 + 测试中继 + 这是聊天中继地址,无法用于连接。 + 为所有人解封订阅者? + 用于新频道 + 使用中继 + 验证 + 通过 %1$s + 你的平台不支持录音 + 等待 + 等待响应 + + 你是订阅者 + 可以分享链接或二维码 —— 任何人均可加入该频道。 + 你通过此中继链接连接至该频道。 + 你的频道 + 你的频道 + 你的个人资料 %1$s 将分享给频道中继和订阅者。中继可以访问频道消息。 + 你的中继地址 + 你的中继名 + 你会停止收到来自该频道的消息。聊天记录将被保留。 + 完整的频道名: + 频道资料存储在订阅者设备和聊天中继上。 + 频道资料已更新 + %d 个频道事件 + 删除了频道 + 被丢弃 (%1$d 次尝试) + 错误: %s + 保存频道资料出错 + 消息错误 + 保存并通知频道订阅者 + 保存频道资料 + 应用在尝试接收这条消息 %1$d 次后删除了它。 + 更新了频道资料 + %2$d 个中继中的 %1$d 个活跃, %3$d 个错误 + %2$d 个中继中的 %1$d 个活跃, %3$d 个被删除 + %2$d 个中继中的 %1$d 个已连接, %3$d 个失灵 + %2$d 个中继中的 %1$d 个已连接, %3$d 个被删除 + %1$d 个中继失灵 + %1$d 个中继不活跃 + 删除了 %1$d 个中继 + 目前不支持添加中继。 + 所有中继均失灵 + 删除了所有中继 + 无法广播 + 频道无活跃中继。请稍后尝试加入。 + 频道暂时不可用 + 不活跃 + 无活跃中继 + 被运营方删除 + 正等到频道所有者添加中继。 + 营业地址 + 频道链接 + 联系地址 + 分享频道出错 + (来自所有者) + 群链接 + 链接签名已验证。 + 一次性链接 + 分享频道… + 经聊天分享 + ⚠️ 签名验证失败:%s。 + (已签名) + 轻触打开 + 发送链接预览可能会将你的 IP 地址暴露给网站。你可以稍后在“隐私”设置中更改此设置。 + 连接达到了未送达消息的上限 + 禁用 + 启用 + 启用链接预览吗? + 错误 + 网络错误 + 中继结果: + 频道首选项 + 仅频道所有者可改变频道首选项。 + 用于单人进行连接的链接 + 频道 + 通过链接或二维码连接 + 创建链接 + 创建你的公开地址 + 邀请好友更简单 👋 + 给任何要和你联系的人 + 私下邀请某人 + 让某人和你连接 + 新建一次性链接 + 非盈利治理 + - 可选发送链接预览\n- 如启用则使用 SOCKS 代理\n- 防止超链接钓鱼\n- 删除链接跟踪。 + 面对面或通过视频通话展示二维码。 + 或使用此二维码 — 打印或在线展示。 + 所有权:你可以运行自己的中继。 + 隐私:对所有者和订阅者。 + 公开频道 — 畅所欲言 🚀 + 可靠性:一个频道众多中继。 + 安全的 web 链接 + 安全性:所有者持有频道密钥。 + 通过任何通讯应用发送链接 — 这是安全的。请求粘贴到 SimpleX 中。 + 和某人交谈 + 让 SimpleX 网络持续。 + 在社交媒体资料、网站或电子邮件签名中使用该地址。 + 我们让连接对新用户更简单。 + 你的公开地址 + 你生来就没有账户。 + 没有人追踪你的谈话内容。没有人绘制你去过的地方的地图。隐私从来都不是一项功能--而是一种生活方式。 + 然后我们转向线上,每个平台都要求你提供一些信息--你的姓名、电话号码、好友列表。我们接受了这样一个事实:与人交流的代价就是让别人知道我们在和谁交流。每一代人,每一代科技,都遵循着这样的模式--电话、电子邮件、即时通讯、社交媒体。这似乎是唯一可行的方式。 + 还有另一种方法。一个没有电话号码、没有用户名、没有账户、没有任何用户身份的网络。一个连接人们并传输加密信息的网络,而无需知道谁连接了。 + 别人家的门锁再好也比不上这里。房东再好也比不上这里,他既尊重你的隐私,又保留着所有访客的记录。你不是客人,你是家。没有国王能闯入--你是主人。 + 你的对话内容始终属于你,就像互联网出现之前一样。网络不是一个你访问的地方,而是一个你创建并拥有的地方。无论你将其设为私密还是公开,任何人都无法将其夺走。 + 人类最古老的自由--与他人交谈而不被监视--建立在不会背叛它的基础设施之上。 + 因为我们摧毁了知道你是谁的权力,因而您的权利永远不会被夺走。 + 在你的网络中自由畅行。 + 允许成员与管理员聊天。 + 允许发送私信给订阅者。 + 允许订阅者与管理员聊天。 + 在您的网络中\n自由驰骋 + 禁止与管理员聊天。 + 与管理员在公开频道中聊天没有端到端加密 — 请只在受信任聊天中继中使用。 + 禁止与成员聊天 + 与管理员聊天 + 禁止订阅者间发送私信。 + 不向新订阅者发送历史记录。 + 允许 + 允许和管理员聊天? + 输入个人资料名… + 开始 + 未发送历史记录给新订阅者。 + 成员可以和管理员聊天。 + 非端到端加密。聊天中继可以看到这些消息。]]> + 迁移 + 网络承诺 + 网络路由器无法\n知道谁和谁交谈 + 无账户。无手机号。无电子邮箱。无 ID。\n最安全的加密。 + 在您的手机上,不在服务器上。 + 打开外部链接? + 私密和安全的消息收发。 + 禁止和管理员聊天。 + 禁止发送私信给订阅者。 + 发送最多 100 条最近消息给新订阅者。 + 通知设置 + 路由器设置 + 订阅者举报 + 订阅者可以添加消息回应。 + 订阅者可以和管理员聊天。 + 订阅者可以不可逆地删除已发送的消息。(24 小时) + 订阅者可以向协管举报消息。 + 订阅者可以发送私信。 + 订阅者可以发送限时消息。 + 订阅者可以发送文件和媒体。 + 订阅者可发送 SimpleX 链接。 + 订阅者可发送语音消息。 + 首个您拥有\n您的联系人和群的网络。 + 已向新订阅者发送了最多 100 条最近的消息。 + 为何打造 SimpleX。 + 您的网络 + 您的个人资料 + 底部栏 + 将通过 SOCKS5 代理请求链接预览。DNS 查询仍可能通过你的 DNS 解析器在本地发生。 + 顶部栏 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 05997a9fec..9ec116058a 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 @@ -847,7 +847,6 @@ 感謝用戶 - 使用 Weblate 的翻譯貢獻! 正在修改聯絡地址為 %s … 受加密的資料庫密碼會再次更新和儲存於金鑰庫。 - SimpleX 是怎樣運作 當發生: \n1. 訊息將在傳送至客戶端後兩天或在伺服器內三十天時過時。 \n2. 訊息解密失敗,因為你或你的聯絡人用了舊的資料庫備份 \n3. 連接被破壞。 只有客戶端裝置儲存個人檔案、聯絡人、群組,和訊息。 請放置你的密碼於安全的地方,如果你遺失了密碼,將不可能修改你的密碼。 @@ -2061,7 +2060,6 @@ 停用自動刪除訊息? 刪除或審查最多 200 條訊息。 於網路和伺服器設定中啟用 Flux 以獲得更好的元資料隱私。 - 配置伺服器營運者 使用條款 更好的隱私和安全性 你可以再試一次。 diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_create_link.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_create_link.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_create_link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_create_link_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_create_link_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_create_link_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_paste_link.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_paste_link.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_paste_link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_paste_link_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_paste_link_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_paste_link_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_connect_via_link_alpha.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_connect_via_link_alpha.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_connect_via_link_alpha.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_connect_via_link_alpha_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_connect_via_link_alpha_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_connect_via_link_alpha_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_create_your_public_address_alpha.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_create_your_public_address_alpha.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_create_your_public_address_alpha.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_create_your_public_address_alpha_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_create_your_public_address_alpha_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_create_your_public_address_alpha_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_invite_someone_privately_alpha.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_invite_someone_privately_alpha.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_invite_someone_privately_alpha.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_invite_someone_privately_alpha_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_invite_someone_privately_alpha_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_invite_someone_privately_alpha_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_let_someone_connect_to_you_alpha.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_let_someone_connect_to_you_alpha.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_let_someone_connect_to_you_alpha.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_let_someone_connect_to_you_alpha_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_let_someone_connect_to_you_alpha_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_let_someone_connect_to_you_alpha_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_light.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_small.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_small.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_small.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_small_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_small_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_small_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_channel.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_channel.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_channel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_channel_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_channel_light.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_channel_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_group.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_group.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_group.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_group_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_group_light.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_group_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_profile.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_profile.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_profile.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_profile_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_profile_light.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_profile_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/intro.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/intro.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/intro.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/intro_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/intro_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/intro_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/network_commitments.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/network_commitments.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/network_commitments.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/network_commitments_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/network_commitments_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/network_commitments_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_light.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_small.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_small.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_small.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_small_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_small_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_small_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_light.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_small.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_small.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_small.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_small_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_small_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_small_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/your_network.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/your_network.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/your_network.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/your_network_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/your_network_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/your_network_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/your_profile.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/your_profile.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/your_profile.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/your_profile_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/your_profile_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/your_profile_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ChatItemsMergerTest.kt b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ChatItemsMergerTest.kt index 18b17b25a9..0d5ec24bff 100644 --- a/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ChatItemsMergerTest.kt +++ b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ChatItemsMergerTest.kt @@ -18,8 +18,7 @@ class ChatItemsMergerTest { val chatState1 = ActiveChatState(splits = splits1) val removed1 = listOf(oldItems[1]) val newItems1 = oldItems - removed1 - val recalc1 = recalculateChatStatePositions(chatState1) - recalc1.removed(removed1.map { Triple(it.id, oldItems.indexOf(removed1[0]), it.isRcvNew) }, newItems1) + chatState1.itemsRemoved(removed1.map { Triple(it.id, oldItems.indexOf(removed1[0]), it.isRcvNew) }, newItems1) assertEquals(1, splits1.value.size) assertEquals(124L, splits1.value.first()) @@ -27,8 +26,7 @@ class ChatItemsMergerTest { val chatState2 = ActiveChatState(splits = splits2) val removed2 = listOf(oldItems[1], oldItems[2]) val newItems2 = oldItems - removed2 - val recalc2 = recalculateChatStatePositions(chatState2) - recalc2.removed(removed2.mapIndexed { index, it -> Triple(it.id, oldItems.indexOf(removed2[index]), it.isRcvNew) }, newItems2) + chatState2.itemsRemoved(removed2.mapIndexed { index, it -> Triple(it.id, oldItems.indexOf(removed2[index]), it.isRcvNew) }, newItems2) assertEquals(1, splits2.value.size) assertEquals(125L, splits2.value.first()) @@ -36,14 +34,12 @@ class ChatItemsMergerTest { val chatState3 = ActiveChatState(splits = splits3) val removed3 = listOf(oldItems[1], oldItems[2], oldItems[3]) val newItems3 = oldItems - removed3 - val recalc3 = recalculateChatStatePositions(chatState3) - recalc3.removed(removed3.mapIndexed { index, it -> Triple(it.id, oldItems.indexOf(removed3[index]), it.isRcvNew) }, newItems3) + chatState3.itemsRemoved(removed3.mapIndexed { index, it -> Triple(it.id, oldItems.indexOf(removed3[index]), it.isRcvNew) }, newItems3) assertEquals(0, splits3.value.size) val splits4 = MutableStateFlow(listOf(123L)) val chatState4 = ActiveChatState(splits = splits4) - val recalc4 = recalculateChatStatePositions(chatState4) - recalc4.cleared() + chatState4.clear() assertEquals(0, splits4.value.size) } diff --git a/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ProviderForGalleryTest.kt b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ProviderForGalleryTest.kt new file mode 100644 index 0000000000..f9311ea6a9 --- /dev/null +++ b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ProviderForGalleryTest.kt @@ -0,0 +1,67 @@ +package chat.simplex.app + +import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.chat.providerForGallery +import kotlinx.datetime.Clock +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +// Regression for PR #6869: scrollToStart() must not rewrite initialChatId. +class ProviderForGalleryTest { + + // Synthetic items pass canShowMedia only when chatModel.connectedToRemote() is true. + @BeforeTest + fun connectChatModelToRemote() { + chatModel.currentRemoteHost.value = RemoteHostInfo( + remoteHostId = 0L, + hostDeviceName = "", + storePath = "", + bindAddress_ = null, + bindPort_ = null, + sessionState = null, + ) + } + + @AfterTest + fun resetChatModel() { + chatModel.currentRemoteHost.value = null + } + + @Test + fun testScrollToStartPreservesAnchor() { + val items = listOf(imageItem(1L), imageItem(2L), imageItem(3L)) + var scrolledTo: Int? = null + val provider = providerForGallery(items, cItemId = 3L) { scrolledTo = it } + + provider.currentPageChanged(provider.initialIndex - 1) + provider.scrollToStart() + provider.onDismiss(0) + + assertEquals(1, scrolledTo) + } + + // Pins the onDismiss early-return contract that testScrollToStartPreservesAnchor + // relies on to read the anchor back through the scrollTo callback. + @Test + fun testOnDismissOnActiveItemDoesNotScroll() { + val items = listOf(imageItem(1L), imageItem(2L), imageItem(3L)) + var scrolledTo: Int? = null + val provider = providerForGallery(items, cItemId = 3L) { scrolledTo = it } + + provider.onDismiss(provider.initialIndex) + + assertEquals(null, scrolledTo) + } + + private fun imageItem(id: Long): ChatItem = + ChatItem( + chatDir = CIDirection.DirectRcv(), + meta = CIMeta.getSample(id, Clock.System.now(), text = ""), + content = CIContent.RcvMsgContent(MsgContent.MCImage(text = "", image = "")), + reactions = emptyList(), + file = CIFile.getSample(fileId = id, fileName = "img-$id.jpg", filePath = "img-$id.jpg"), + ) +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/StoreWindowState.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/StoreWindowState.kt index 2a1a26df95..e4866c845d 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/StoreWindowState.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/StoreWindowState.kt @@ -3,11 +3,12 @@ package chat.simplex.common import chat.simplex.common.model.json import chat.simplex.common.platform.appPreferences import chat.simplex.common.platform.desktopPlatform +import chat.simplex.common.ui.theme.DEFAULT_WINDOW_WIDTH import kotlinx.serialization.* @Serializable data class WindowPositionSize( - val width: Int = 1366, + val width: Int = DEFAULT_WINDOW_WIDTH.value.toInt(), val height: Int = 768, val x: Int = 0, val y: Int = 0, 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 index 41964b7d18..557dabd2e4 100644 --- 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 @@ -86,8 +86,8 @@ actual fun PlatformTextField( // Different padding here is for a text that is considered RTL with non-RTL locale set globally. // In this case padding from right side should be bigger val startEndPadding = if (cs.message.text.isEmpty() && showVoiceButton && isRtlByCharacters && isLtrGlobally) 95.dp else 50.dp - val startPadding = if (isRtlByCharacters && isLtrGlobally) startEndPadding else 0.dp - val endPadding = if (isRtlByCharacters && isLtrGlobally) 0.dp else startEndPadding + val startPadding = 0.dp + val endPadding = startEndPadding val padding = PaddingValues(startPadding, 12.dp, endPadding, 0.dp) var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message.text, selection = cs.message.selection)) } val textFieldValue = textFieldValueState.copy(text = cs.message.text, selection = cs.message.selection) 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 index 59d71a83f1..8d26f2f085 100644 --- 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 @@ -14,6 +14,8 @@ import java.util.* import kotlin.math.max internal val vlcFactory: MediaPlayerFactory by lazy { MediaPlayerFactory() } +// No hardware acceleration - more secure for previews +internal val vlcPreviewFactory: MediaPlayerFactory by lazy { MediaPlayerFactory("--avcodec-hw=none") } actual class RecorderNative: RecorderInterface { private var player: MediaPlayer? = null 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 index c5a38ec4a1..90c80d3b2a 100644 --- 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 @@ -225,9 +225,9 @@ actual class VideoPlayer actual constructor( player.media().startPaused(uri.toFile().absolutePath) val start = System.currentTimeMillis() var snap: BufferedImage? = null - while (snap == null && start + 5000 > System.currentTimeMillis()) { + while (snap == null && start + 1500 > System.currentTimeMillis()) { snap = player.snapshots()?.get() - delay(10) + delay(50) } val orientation = player.media().info().videoTracks().firstOrNull()?.orientation() if (orientation == null) { @@ -265,7 +265,9 @@ actual class VideoPlayer actual constructor( mediaPlayer().events().addMediaPlayerEventListener(object: MediaPlayerEventAdapter() { override fun mediaPlayerReady(mediaPlayer: MediaPlayer?) { playerThread.execute { - mediaPlayer?.audio()?.setVolume(100) + // Do not call setVolume here: on Windows VLCJ routes it through WASAPI ISimpleAudioVolume, + // which resets SimpleX Chat's per-app volume in the Windows Volume Mixer on every playback + // (VLCJ issue #985). A fresh VLCJ MediaPlayer already defaults to volume 100, so this was redundant. mediaPlayer?.audio()?.isMute = false } } @@ -278,7 +280,7 @@ actual class VideoPlayer actual constructor( private fun putPlayer(player: Component) = playersPool.add(player) - private fun getOrCreateHelperPlayer(): CallbackMediaPlayerComponent = helperPlayersPool.removeFirstOrNull() ?: CallbackMediaPlayerComponent(MediaPlayerSpecs.callbackMediaPlayerSpec().apply { withFactory(vlcFactory) }) + private fun getOrCreateHelperPlayer(): CallbackMediaPlayerComponent = helperPlayersPool.removeFirstOrNull() ?: CallbackMediaPlayerComponent(MediaPlayerSpecs.callbackMediaPlayerSpec().apply { withFactory(vlcPreviewFactory) }) private fun putHelperPlayer(player: CallbackMediaPlayerComponent) = helperPlayersPool.add(player) } } diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 1926f35f0f..09cf90553f 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,13 +24,13 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.5-beta.7 -android.version_code=339 +android.version_name=6.5.1 +android.version_code=347 android.bundle=false -desktop.version_name=6.5-beta.7 -desktop.version_code=134 +desktop.version_name=6.5.1 +desktop.version_code=142 kotlin.version=2.1.20 gradle.plugin.version=8.7.0 diff --git a/apps/multiplatform/local.properties.example b/apps/multiplatform/local.properties.example index 8fa9a47963..9aa560d839 100644 --- a/apps/multiplatform/local.properties.example +++ b/apps/multiplatform/local.properties.example @@ -3,6 +3,8 @@ enable_debuggable=true application_id.suffix=.debug app.name=SimpleX Debug +#simplex.assets.dir=path/to/assets + #desktop.mac.signing.identity=SimpleX Chat Ltd #desktop.mac.signing.keychain=/path/to/simplex.keychain #desktop.mac.notarization.apple_id=example@example.com diff --git a/apps/multiplatform/settings.gradle.kts b/apps/multiplatform/settings.gradle.kts index 40446f1958..50a50d531d 100644 --- a/apps/multiplatform/settings.gradle.kts +++ b/apps/multiplatform/settings.gradle.kts @@ -3,7 +3,6 @@ pluginManagement { google() gradlePluginPortal() mavenCentral() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } plugins { diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index 45c0b84cc6..bfbc025a49 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -33,10 +33,10 @@ import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Directory.Store import Simplex.Chat.Controller -import Simplex.Chat.Markdown (displayNameTextP) +import Simplex.Chat.Markdown (MarkdownList, displayNameTextP) import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent -import Simplex.Chat.Protocol (MsgContent (..)) +import Simplex.Chat.Protocol (LinkOwnerSig, MsgChatLink, MsgContent (..)) import Simplex.Chat.Types import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (AgentErrorType (..)) @@ -49,6 +49,7 @@ data DirectoryEvent | DEGroupInvitation {contact :: Contact, groupInfo :: GroupInfo, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} | DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember} | DEGroupUpdated {member :: GroupMember, fromGroup :: GroupInfo, toGroup :: GroupInfo} + | DEGroupLinkCheck GroupInfo | DEPendingMember GroupInfo GroupMember | DEPendingMemberMsg GroupInfo GroupMember ChatItemId Text | DEContactRoleChanged GroupInfo ContactId GroupMemberRole -- contactId here is the contact whose role changed @@ -57,6 +58,8 @@ data DirectoryEvent | DEContactLeftGroup ContactId GroupInfo | DEServiceRemovedFromGroup GroupInfo | DEGroupDeleted GroupInfo + | DEChatLinkReceived {contact :: Contact, chatItemId :: ChatItemId, chatLink :: MsgChatLink, ownerSig :: Maybe LinkOwnerSig} + | DEMemberUpdated {groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember} | DEUnsupportedMessage Contact ChatItemId | DEItemEditIgnored Contact | DEItemDeleteIgnored Contact @@ -91,11 +94,14 @@ crDirectoryEvent_ = \case CEvtLeftMember {groupInfo, member} -> (`DEContactLeftGroup` groupInfo) <$> memberContactId member CEvtDeletedMemberUser {groupInfo} -> Just $ DEServiceRemovedFromGroup groupInfo CEvtGroupDeleted {groupInfo} -> Just $ DEGroupDeleted groupInfo + CEvtUnknownMemberAnnounced {groupInfo, unknownMember, announcedMember} -> Just $ DEMemberUpdated {groupInfo, fromMember = unknownMember, toMember = announcedMember} + CEvtGroupMemberUpdated {groupInfo, fromMember, toMember} -> Just $ DEMemberUpdated {groupInfo, fromMember, toMember} CEvtChatItemUpdated {chatItem = AChatItem _ SMDRcv (DirectChat ct) _} -> Just $ DEItemEditIgnored ct CEvtChatItemsDeleted {chatItemDeletions = ((ChatItemDeletion (AChatItem _ SMDRcv (DirectChat ct) _) _) : _), byUser = False} -> Just $ DEItemDeleteIgnored ct - CEvtNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, meta = CIMeta {itemLive}}) : _} -> + CEvtNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, formattedText = ft, meta = CIMeta {itemLive}}) : _} -> Just $ case (mc, itemLive) of - (MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly (directoryCmdP <* A.endOfInput) $ T.dropWhileEnd isSpace t + (MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly (directoryCmdP ft <* A.endOfInput) $ T.dropWhileEnd isSpace t + (MCChat {chatLink, ownerSig}, Nothing) -> DEChatLinkReceived {contact = ct, chatItemId = ciId, chatLink, ownerSig} _ -> DEUnsupportedMessage ct ciId where ciId = chatItemId' ci @@ -149,7 +155,7 @@ data DirectoryHelpSection = DHSRegistration | DHSCommands data DirectoryCmd (r :: DirectoryRole) where DCHelp :: DirectoryHelpSection -> DirectoryCmd 'DRUser - DCSearchGroup :: Text -> DirectoryCmd 'DRUser + DCSearchGroup :: Text -> Maybe MarkdownList -> DirectoryCmd 'DRUser DCSearchNext :: DirectoryCmd 'DRUser DCAllGroups :: DirectoryCmd 'DRUser DCRecentGroups :: DirectoryCmd 'DRUser @@ -181,11 +187,11 @@ data ADirectoryCmd = forall r. ADC (SDirectoryRole r) (DirectoryCmd r) deriving instance Show ADirectoryCmd -directoryCmdP :: Parser ADirectoryCmd -directoryCmdP = +directoryCmdP :: Maybe MarkdownList -> Parser ADirectoryCmd +directoryCmdP ft = (A.char '/' *> cmdStrP) <|> (A.char '.' $> ADC SDRUser DCSearchNext) - <|> (ADC SDRUser . DCSearchGroup <$> A.takeText) + <|> (ADC SDRUser . (`DCSearchGroup` ft) <$> A.takeText) where cmdStrP = (tagP >>= \(ADCT u t) -> ADC u <$> (cmdP t <|> pure (DCCommandError t))) @@ -304,7 +310,7 @@ directoryCmdP = directoryCmdTag :: DirectoryCmd r -> Text directoryCmdTag = \case DCHelp _ -> "help" - DCSearchGroup _ -> "search" + DCSearchGroup {} -> "search" DCSearchNext -> "next" DCAllGroups -> "all" DCRecentGroups -> "new" diff --git a/apps/simplex-directory-service/src/Directory/Listing.hs b/apps/simplex-directory-service/src/Directory/Listing.hs index 0d4e8d351c..ef093020bb 100644 --- a/apps/simplex-directory-service/src/Directory/Listing.hs +++ b/apps/simplex-directory-service/src/Directory/Listing.hs @@ -27,7 +27,7 @@ import Data.List (isPrefixOf) import Data.Maybe (catMaybes, fromMaybe) import Data.Text (Text) import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) +import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Time.Clock import Data.Time.Clock.System import Data.Time.Format.ISO8601 (iso8601Show) @@ -53,16 +53,24 @@ listingImageFolder :: String listingImageFolder = "images" data DirectoryEntryType = DETGroup - { admission :: Maybe GroupMemberAdmission, + { groupType :: Maybe GroupType, + admission :: Maybe GroupMemberAdmission, summary :: GroupSummary } $(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "DET") ''DirectoryEntryType) +data PublicLink = PublicLink + { connFullLink :: Maybe ConnReqContact, + connShortLink :: Maybe ShortLinkContact + } + +$(JQ.deriveJSON defaultJSON ''PublicLink) + data DirectoryEntry = DirectoryEntry { entryType :: DirectoryEntryType, displayName :: Text, - groupLink :: CreatedLinkContact, + groupLink :: PublicLink, shortDescr :: Maybe MarkdownList, welcomeMessage :: Maybe MarkdownList, imageFile :: Maybe String, @@ -90,8 +98,15 @@ recentRoundedTime roundTo now t groupDirectoryEntry :: UTCTime -> GroupInfo -> Maybe GroupLink -> Maybe (DirectoryEntry, Maybe (FilePath, ImageFileData)) groupDirectoryEntry now GroupInfo {groupProfile, chatTs, createdAt, groupSummary} gLink_ = - let GroupProfile {displayName, shortDescr, description, image, memberAdmission} = groupProfile - entryType = DETGroup memberAdmission groupSummary + let GroupProfile {displayName, shortDescr, description, image, memberAdmission, publicGroup} = groupProfile + gt = (\PublicGroupProfile {groupType} -> groupType) <$> publicGroup + entryType = DETGroup gt memberAdmission groupSummary + description' = case publicGroup of + Just PublicGroupProfile {groupType = gt', groupLink = sLnk} -> + let gtStr = case gt' of GTChannel -> "channel"; _ -> "group" + linkLine = "Link to join the " <> gtStr <> " " <> displayName <> ": " <> decodeUtf8 (strEncode sLnk) + in Just $ maybe linkLine (<> "\n\n" <> linkLine) description + Nothing -> description entry groupLink = let de = DirectoryEntry @@ -99,22 +114,30 @@ groupDirectoryEntry now GroupInfo {groupProfile, chatTs, createdAt, groupSummary displayName, groupLink, shortDescr = toFormattedText <$> shortDescr, - welcomeMessage = toFormattedText <$> description, + welcomeMessage = toFormattedText <$> description', imageFile = fst <$> imgData, activeAt = recentRoundedTime 900 now $ fromMaybe createdAt chatTs, createdAt = recentRoundedTime 86400 now createdAt } imgData = imgFileData groupLink =<< image in (de, imgData) - in (entry . connLinkContact) <$> gLink_ + in case publicGroup of + Just PublicGroupProfile {groupLink = sLnk} -> + Just $ entry $ PublicLink Nothing (Just sLnk) + Nothing -> + entry . toPublicLink . connLinkContact <$> gLink_ where - imgFileData :: CreatedConnLink 'CMContact -> ImageData -> Maybe (FilePath, ByteString) - imgFileData groupLink (ImageData img) = + toPublicLink (CCLink fullLink shortLink) = PublicLink (Just fullLink) shortLink + imgFileData :: PublicLink -> ImageData -> Maybe (FilePath, ByteString) + imgFileData PublicLink {connFullLink, connShortLink} (ImageData img) = let (img', imgExt) = fromMaybe (img, ".jpg") $ (,".jpg") <$> T.stripPrefix "data:image/jpg;base64," img <|> (,".png") <$> T.stripPrefix "data:image/png;base64," img - imgName = B.unpack $ B64URL.encodeUnpadded $ BA.convert $ (CH.hash :: ByteString -> Digest MD5) $ strEncode (connFullLink groupLink) + linkHash = case connFullLink of + Just fl -> strEncode fl + Nothing -> maybe "" strEncode connShortLink + imgName = B.unpack $ B64URL.encodeUnpadded $ BA.convert $ (CH.hash :: ByteString -> Digest MD5) linkHash imgFile = listingImageFolder imgName <> imgExt in case B64.decode $ encodeUtf8 img' of Right img'' -> Just (imgFile, img'') diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index a44e55e376..5d51023781 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -43,6 +43,7 @@ data DirectoryOpts = DirectoryOpts runCLI :: Bool, searchResults :: Int, webFolder :: Maybe FilePath, + linkCheckInterval :: Int, testing :: Bool } @@ -168,6 +169,14 @@ directoryOpts appDir defaultDbName = do <> metavar "WEB_FOLDER" <> help "Folder to store static web assets" ) + linkCheckInterval <- + option + auto + ( long "link-check-interval" + <> metavar "SECONDS" + <> help "Interval in seconds to check public group link data (default: 1800)" + <> value 1800 + ) pure DirectoryOpts { coreOptions, @@ -189,6 +198,7 @@ directoryOpts appDir defaultDbName = do runCLI, searchResults = 10, webFolder, + linkCheckInterval, testing = False } diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 34b63ff06a..6e414ef011 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -18,7 +18,7 @@ module Directory.Service ) where -import Control.Concurrent (forkIO) +import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent.STM import Control.Exception (SomeException, try) import Control.Logger.Simple @@ -31,7 +31,7 @@ import Data.Either (fromRight) import Data.List (find, intercalate) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.Map.Strict as M -import Data.Maybe (fromMaybe, isJust, isNothing) +import Data.Maybe (fromMaybe, isJust, isNothing, maybeToList) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T @@ -51,12 +51,12 @@ import Simplex.Chat.Bot import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller import Simplex.Chat.Core -import Simplex.Chat.Markdown (Format (..), FormattedText (..), parseMaybeMarkdownList, viewName) +import Simplex.Chat.Markdown (Format (..), FormattedText (..), SimplexLinkType (..), parseMaybeMarkdownList, viewName) import Simplex.Chat.Messages import Simplex.Chat.Options -import Simplex.Chat.Protocol (MsgContent (..), memberSupportVoiceVersion) +import Simplex.Chat.Protocol (GroupShortLinkData (..), LinkOwnerSig (..), MsgChatLink (..), MsgContent (..), memberSupportVoiceVersion) import Simplex.Chat.Store.Direct (getContact) -import Simplex.Chat.Store.Groups (getGroupLink, getGroupMember, setGroupCustomData) -- TODO remove setGroupCustomData +import Simplex.Chat.Store.Groups (getGroupLink, getGroupMember, getGroupMemberByMemberId, setGroupCustomData) -- TODO remove setGroupCustomData import Simplex.Chat.Store.Profiles (GroupLinkInfo (..), getGroupLinkInfo) import Simplex.Chat.Store.Shared (StoreError (..)) import Simplex.Chat.Terminal (terminalChatConfig) @@ -65,9 +65,10 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.View (serializeChatError, serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) -import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnectionLink (..), CreatedConnLink (..), SConnectionMode (..), sameConnReqContact, sameShortLinkContact) +import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ACreatedConnLink (..), AgentErrorType (..), ConnectionLink (..), CreatedConnLink (..), SConnectionMode (..), sameConnReqContact, sameShortLinkContact) import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Protocol (ErrorType (..)) import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Util (eitherToMaybe, raceAny_, safeDecodeUtf8, tshow, unlessM, (<$$>)) @@ -99,7 +100,9 @@ data ServiceState = ServiceState { searchRequests :: TMap ContactId SearchRequest, blockedWordsCfg :: BlockedWordsConfig, pendingCaptchas :: TMap GroupMemberId PendingCaptcha, - updateListingsJob :: TMVar ChatController + serviceCC :: TMVar ChatController, + eventQ :: TQueue DirectoryEvent, + updateListingsJob :: TMVar () } data CaptchaMode = CMText | CMAudio @@ -125,8 +128,10 @@ newServiceState opts = do searchRequests <- TM.emptyIO blockedWordsCfg <- readBlockedWordsConfig opts pendingCaptchas <- TM.emptyIO + serviceCC <- newEmptyTMVarIO + eventQ <- newTQueueIO updateListingsJob <- newEmptyTMVarIO - pure ServiceState {searchRequests, blockedWordsCfg, pendingCaptchas, updateListingsJob} + pure ServiceState {searchRequests, blockedWordsCfg, pendingCaptchas, serviceCC, eventQ, updateListingsJob} welcomeGetOpts :: IO DirectoryOpts welcomeGetOpts = do @@ -150,9 +155,8 @@ welcomeGetOpts = do directoryServiceCLI :: DirectoryLog -> DirectoryOpts -> IO () directoryServiceCLI st opts = do - env <- newServiceState opts - eventQ <- newTQueueIO - let eventHook cc resp = atomically $ resp <$ writeTQueue eventQ (cc, resp) + env@ServiceState {eventQ} <- newServiceState opts + let eventHook _cc resp = atomically $ resp <$ mapM_ (writeTQueue eventQ) (crDirectoryEvent resp) chatHooks = defaultChatHooks { preStartHook = Just $ directoryPreStartHook opts, @@ -162,31 +166,50 @@ directoryServiceCLI st opts = do } raceAny_ $ [ simplexChatCLI' terminalChatConfig {chatHooks} (mkChatOpts opts) Nothing, - processEvents eventQ env + processEvents env ] - <> updateListingsThread_ opts env + <> maybeToList (updateListingsThread_ opts env) + <> maybeToList (linkCheckThread_ opts env) where - processEvents eventQ env = forever $ do - (cc, resp) <- atomically $ readTQueue eventQ + processEvents env@ServiceState {eventQ} = do + cc <- atomically $ readTMVar $ serviceCC env u_ <- readTVarIO (currentUser cc) - forM_ u_ $ \user -> directoryServiceEvent st opts env user cc resp + forM_ u_ $ \user -> + forever $ do + event <- atomically $ readTQueue eventQ + directoryServiceEvent st opts env user cc event updateListingDelay :: Int updateListingDelay = 5 * 60 * 1000000 -- update every 5 minutes -updateListingsThread_ :: DirectoryOpts -> ServiceState -> [IO ()] -updateListingsThread_ opts env = maybe [] (\f -> [updateListingsThread f]) $ webFolder opts +updateListingsThread_ :: DirectoryOpts -> ServiceState -> Maybe (IO ()) +updateListingsThread_ opts env = updateListingsThread <$> webFolder opts where updateListingsThread f = do - cc <- atomically $ takeTMVar $ updateListingsJob env + cc <- atomically $ readTMVar $ serviceCC env forever $ do u <- readTVarIO $ currentUser cc forM_ u $ \user -> updateGroupListingFiles cc user f delay <- registerDelay updateListingDelay atomically $ void (takeTMVar $ updateListingsJob env) `orElse` unlessM (readTVar delay) retry -listingsUpdated :: ServiceState -> ChatController -> IO () -listingsUpdated env = void . atomically . tryPutTMVar (updateListingsJob env) +listingsUpdated :: ServiceState -> IO () +listingsUpdated env = void $ atomically $ tryPutTMVar (updateListingsJob env) () + +linkCheckThread_ :: DirectoryOpts -> ServiceState -> Maybe (IO ()) +linkCheckThread_ opts env@ServiceState {eventQ} + | linkCheckInterval opts > 0 = Just $ do + cc <- atomically $ readTMVar $ serviceCC env + forever $ do + threadDelay $ linkCheckInterval opts * 1000000 + u <- readTVarIO $ currentUser cc + forM_ u $ \user -> + withDB' "linkCheckThread" cc (\db -> getAllGroupRegs_ db user) >>= \case + Left e -> logError $ "linkCheckThread error: " <> T.pack e + Right grs -> forM_ grs $ \(gInfo, gr) -> + unless (groupRemoved $ groupRegStatus gr) $ + atomically $ writeTQueue eventQ $ DEGroupLinkCheck gInfo + | otherwise = Nothing directoryPreStartHook :: DirectoryOpts -> ChatController -> IO () directoryPreStartHook opts ChatController {config, chatStore} = runDirectoryMigrations opts config chatStore @@ -197,7 +220,8 @@ directoryPostStartHook opts@DirectoryOpts {noAddress, testing} env cc = Nothing -> putStrLn "No current user" >> exitFailure Just User {userId, profile = p@LocalProfile {preferences}} -> do unless noAddress $ initializeBotAddress' (not testing) cc - listingsUpdated env cc + void $ atomically $ tryPutTMVar (serviceCC env) cc + listingsUpdated env let cmds = fromMaybe [] $ preferences >>= commands_ unless (cmds == directoryCommands) $ do let prefs = (fromMaybe emptyChatPrefs preferences) {files = Just FilesPreference {allow = FANo}, commands = Just directoryCommands} :: Preferences @@ -226,7 +250,7 @@ directoryCommands = directoryService :: DirectoryLog -> DirectoryOpts -> ChatConfig -> IO () directoryService st opts cfg = do - env <- newServiceState opts + env@ServiceState {eventQ} <- newServiceState opts let chatHooks = defaultChatHooks { preStartHook = Just $ directoryPreStartHook opts, @@ -235,12 +259,15 @@ directoryService st opts cfg = do } simplexChatCore cfg {chatHooks} (mkChatOpts opts) $ \user cc -> raceAny_ $ - [ forever $ void getLine, - forever $ do + [ forever $ do (_, resp) <- atomically . readTBQueue $ outputQ cc - directoryServiceEvent st opts env user cc resp + mapM_ (atomically . writeTQueue eventQ) $ crDirectoryEvent resp, + forever $ do + event <- atomically $ readTQueue eventQ + directoryServiceEvent st opts env user cc event ] - <> updateListingsThread_ opts env + <> maybeToList (updateListingsThread_ opts env) + <> maybeToList (linkCheckThread_ opts env) acceptMemberHook :: DirectoryOpts -> ServiceState -> GroupInfo -> GroupLinkInfo -> Profile -> IO (Either GroupRejectionReason (GroupAcceptance, GroupMemberRole)) acceptMemberHook @@ -283,13 +310,13 @@ readBlockedWordsConfig DirectoryOpts {blockedFragmentsFile, blockedWordsFile, na unless testing $ putStrLn $ "Blocked fragments: " <> show (length blockedFragments) <> ", blocked words: " <> show (length blockedWords) <> ", spelling rules: " <> show (M.size spelling) pure BlockedWordsConfig {blockedFragments, blockedWords, extensionRules, spelling} -directoryServiceEvent :: DirectoryLog -> DirectoryOpts -> ServiceState -> User -> ChatController -> Either ChatError ChatEvent -> IO () -directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName, ownersGroup, searchResults} env@ServiceState {searchRequests} user@User {userId} cc event = - forM_ (crDirectoryEvent event) $ \case +directoryServiceEvent :: DirectoryLog -> DirectoryOpts -> ServiceState -> User -> ChatController -> DirectoryEvent -> IO () +directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName, ownersGroup, searchResults} env@ServiceState {searchRequests} user@User {userId} cc = \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 {member, fromGroup, toGroup} -> deGroupUpdated member fromGroup toGroup + DEGroupLinkCheck g -> deGroupLinkCheck g DEPendingMember g m -> dePendingMember g m DEPendingMemberMsg g m ciId t -> dePendingMemberMsg g m ciId t DEContactRoleChanged g ctId role -> deContactRoleChanged g ctId role @@ -298,6 +325,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName DEContactLeftGroup ctId g -> deContactLeftGroup ctId g DEServiceRemovedFromGroup g -> deServiceRemovedFromGroup g DEGroupDeleted g -> deGroupDeleted g + DEChatLinkReceived {contact = ct, chatLink, ownerSig} -> deChatLinkReceived ct chatLink ownerSig + DEMemberUpdated {groupInfo = g, fromMember, toMember} -> deMemberUpdated g fromMember toMember DEUnsupportedMessage _ct _ciId -> pure () DEItemEditIgnored _ct -> pure () DEItemDeleteIgnored _ct -> pure () @@ -325,7 +354,19 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let msg = "Error: " <> err <> ", group: " <> tshow groupId <> " " <> localDisplayName <> ", " <> T.pack e notifyAdminUsers msg logError msg - groupInfoText p@GroupProfile {description = d} = groupNameDescr p <> maybe "" ("\nWelcome message:\n" <>) d + groupInfoText p@GroupProfile {description = d, publicGroup} = groupNameDescr p <> maybe "" ("\nWelcome message:\n" <>) d <> linkToJoin + where + linkToJoin = case publicGroup of + Just pg@PublicGroupProfile {groupLink} -> + "\nLink to join " <> groupTypeStr' pg <> ": " <> strEncodeTxt groupLink + <> "\nYou need SimpleX Chat app v6.5 to join." + Nothing -> "" + membersCountStr GroupProfile {publicGroup} GroupSummary {currentMembers, publicMemberCount} = + let count = fromMaybe currentMembers publicMemberCount + label = case publicGroup of + Just PublicGroupProfile {groupType = GTChannel} -> " subscribers" + _ -> " members" + in tshow count <> label knockingStr :: Maybe GroupMemberAdmission -> [Text] knockingStr = \case Just GroupMemberAdmission {review = Just MCAll} -> ["New members are reviewed by admins"] @@ -342,6 +383,9 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName groupReference' groupId displayName = "ID " <> tshow groupId <> " (" <> displayName <> ")" groupAlreadyListed GroupInfo {groupProfile = p} = "The group " <> groupNameDescr p <> " is already listed in the directory, please choose another name." + ifPublicGroup :: GroupInfo -> IO () -> IO () -> IO () + ifPublicGroup GroupInfo {groupProfile = GroupProfile {publicGroup}} reject action = + if isJust publicGroup then reject else action getDuplicateGroup :: GroupInfo -> IO (Either String DuplicateGroup) getDuplicateGroup GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = @@ -375,7 +419,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName sendMessage cc ct $ ("Welcome to " <> serviceName <> "!\n\n") <> "🔍 Send search string to find groups - try _security_.\n\ - \/help - how to submit your group.\n\ + \/help - how to submit your group or channel.\n\ \/new - recent groups.\n\n\ \[Directory rules](https://simplex.chat/docs/directory.html)." @@ -461,37 +505,68 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName byMember = case memberContactId m of Just ctId | ctId `isOwner` gr -> "" -- group registration owner, not any group owner. _ -> " by " <> mName -- owner notification from directory will include the name. - case groupRegStatus of - GRSPendingConfirmation -> pure () - GRSProposed -> pure () - GRSPendingUpdate -> - groupProfileUpdate >>= \case - GPNoServiceLink -> - notifyOwner gr $ "The profile updated for " <> userGroupRef <> byMember <> ", but the group link is not added to the welcome message." - GPServiceLinkAdded _ -> groupLinkAdded gr byMember - GPServiceLinkRemoved -> - notifyOwner gr $ - "The group link of " <> userGroupRef <> " is removed from the welcome message" <> byMember <> ", please add it." - GPHasServiceLink {} -> groupLinkAdded gr byMember - GPServiceLinkError -> do - notifyOwner gr $ - ("Error: " <> serviceName <> " has no group link for " <> userGroupRef) - <> " after profile was updated" - <> byMember - <> ". Please report the error to the developers." - logError $ "Error: no group link for " <> userGroupRef - GRSPendingApproval n -> processProfileChange gr byMember False $ n + 1 - GRSActive -> processProfileChange gr byMember True 1 - GRSSuspended -> processProfileChange gr byMember False 1 - GRSSuspendedBadRoles -> processProfileChange gr byMember False 1 - GRSRemoved -> pure () + case publicGroup p' of + Just pg -> case groupRegStatus of + GRSPendingApproval n -> publicGroupProfileChange pg gr byMember $ n + 1 + GRSActive -> publicGroupProfileChange pg gr byMember 1 + _ -> pure () + Nothing -> case groupRegStatus of + GRSPendingConfirmation -> pure () + GRSProposed -> pure () + GRSPendingUpdate -> + groupProfileUpdate >>= \case + GPNoServiceLink -> + notifyOwner gr $ "The profile updated for " <> userGroupRef <> byMember <> ", but the group link is not added to the welcome message." + GPServiceLinkAdded _ -> groupLinkAdded gr byMember + GPServiceLinkRemoved -> + notifyOwner gr $ + "The group link of " <> userGroupRef <> " is removed from the welcome message" <> byMember <> ", please add it." + GPHasServiceLink {} -> groupLinkAdded gr byMember + GPServiceLinkError -> do + notifyOwner gr $ + ("Error: " <> serviceName <> " has no group link for " <> userGroupRef) + <> " after profile was updated" + <> byMember + <> ". Please report the error to the developers." + logError $ "Error: no group link for " <> userGroupRef + GRSPendingApproval n -> processProfileChange gr byMember False $ n + 1 + GRSActive -> processProfileChange gr byMember True 1 + GRSSuspended -> processProfileChange gr byMember False 1 + GRSSuspendedBadRoles -> processProfileChange gr byMember False 1 + GRSRemoved -> pure () where GroupInfo {groupId, groupProfile = p} = fromGroup GroupInfo {groupProfile = p'} = toGroup sameProfile - GroupProfile {displayName = n, fullName = fn, shortDescr = sd, image = i, description = d, memberAdmission = ma} - GroupProfile {displayName = n', fullName = fn', shortDescr = sd', image = i', description = d', memberAdmission = ma'} = - n == n' && fn == fn' && i == i' && sd == sd' && (T.words <$> d) == (T.words <$> d') && ma == ma' + GroupProfile {displayName = n, fullName = fn, shortDescr = sd, image = i, description = d, memberAdmission = ma, publicGroup = pg} + GroupProfile {displayName = n', fullName = fn', shortDescr = sd', image = i', description = d', memberAdmission = ma', publicGroup = pg'} = + n == n' && fn == fn' && i == i' && sd == sd' && (T.words <$> d) == (T.words <$> d') && ma == ma' && pg == pg' + publicGroupProfileChange pg@PublicGroupProfile {groupLink} gr byMember n' = do + let gt = groupTypeStr' pg + userGroupRef = userGroupReference gr toGroup + groupRef = groupReference toGroup + link = ACL SCMContact $ CLShort groupLink + updatedNotification gr' g' = do + notifyOwner gr' $ + ("The " <> gt <> " " <> userGroupRef <> " is updated" <> byMember) + <> ".\nIt is hidden from the directory until approved." + notifyAdminUsers $ "The " <> gt <> " " <> groupRef <> " is updated" <> byMember <> "." + sendToApprove g' gr' n' + sendChatCmd cc (APIConnectPlan userId (Just link) True Nothing) >>= \case + Right (CRConnectionPlan _ _ (CPGroupLink (GLPKnown {groupInfo = g'}))) -> + case dbOwnerMemberId gr of + Just ownerGMId -> + withDB "getGroupMember" cc (\db -> withExceptT show $ getGroupMember db (vr cc) user groupId ownerGMId) >>= \case + Right ownerMember + | let GroupMember {memberRole = role} = ownerMember, role >= GROwner -> + setGroupStatus notifyAdminUsers st env cc groupId (GRSPendingApproval n') (`updatedNotification` g') + | otherwise -> do + setGroupStatus notifyAdminUsers st env cc groupId GRSSuspendedBadRoles $ \_ -> pure () + notifyOwner gr $ "The registration owner is no longer an owner. Registration suspended." + Left _ -> logError $ "could not find owner member for " <> groupRef + Nothing -> logError $ "no owner member set for " <> groupRef + _ -> + setGroupStatus notifyAdminUsers st env cc groupId (GRSPendingApproval n') (`updatedNotification` toGroup) groupLinkAdded gr byMember = getDuplicateGroup toGroup >>= \case Left e -> notifyOwner gr $ "Error: getDuplicateGroup. Please notify the developers.\n" <> T.pack e @@ -644,7 +719,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName -- /audio is matched as text, not as DirectoryCmd, because it is only valid -- in group context at captcha stage, while DirectoryCmd is for DM commands. isAudioCmd = T.strip msgText == "/audio" - cmd = fromRight (ADC SDRUser DCUnknownCommand) $ A.parseOnly (directoryCmdP <* A.endOfInput) $ T.strip msgText + cmd = fromRight (ADC SDRUser DCUnknownCommand) $ A.parseOnly (directoryCmdP Nothing <* A.endOfInput) $ T.strip msgText atomically (TM.lookup gmId $ pendingCaptchas env) >>= \case Nothing | isAudioCmd && canSendVoiceCaptcha g m -> sendMemberCaptcha g m (Just ciId) noCaptcha 0 CMAudio @@ -661,7 +736,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName sendComposedMessages_ cc sendRef [(Just ciId, MCText audioAlreadyEnabled)] else sendComposedMessages_ cc sendRef [(Just ciId, MCText voiceCaptchaUnavailable)] | otherwise -> case cmd of - ADC SDRUser (DCSearchGroup _) -> do + ADC SDRUser (DCSearchGroup {}) -> do ts <- getCurrentTime if | ts `diffUTCTime` sentAt > captchaTTL -> sendMemberCaptcha g m (Just ciId) captchaExpired (attempts - 1) captchaMode @@ -704,17 +779,59 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName useMemberFilter image $ passCaptcha a sendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO () - sendToApprove GroupInfo {groupId, groupProfile = p@GroupProfile {displayName, image = image'}, groupSummary} GroupReg {dbContactId, promoted} gaId = do + sendToApprove GroupInfo {groupId, groupProfile = p@GroupProfile {displayName, image = image', publicGroup = pg_}, groupSummary} GroupReg {dbContactId, promoted} gaId = do ct_ <- getContact' cc user dbContactId - let membersStr = "_" <> tshow (currentMembers groupSummary) <> " members_\n" + let gt = maybe "group" groupTypeStr' pg_ + membersStr = "_" <> membersCountStr p groupSummary <> "_\n" text = - either (\_ -> "The group ID " <> tshow groupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow groupId <> ": ") ct_ + either (\_ -> "The " <> gt <> " ID " <> tshow groupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the " <> gt <> " ID " <> tshow groupId <> ": ") ct_ <> ("\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:") msg = maybe (MCText text) (\image -> MCImage {text, image}) image' withAdminUsers $ \cId -> do let approveCmd = MCText $ "/approve " <> tshow groupId <> ":" <> viewName displayName <> " " <> tshow gaId <> if promoted then " promote=on" else "" sendComposedMessages cc (SRDirect cId) [msg, approveCmd] + deGroupLinkCheck :: GroupInfo -> IO () + deGroupLinkCheck gInfo@GroupInfo {groupId, groupProfile = GroupProfile {publicGroup = pg_}, groupSummary = summary} = + withGroupReg gInfo "link check" $ \gr@GroupReg {groupRegStatus, dbOwnerMemberId} -> + forM_ pg_ $ \pg@PublicGroupProfile {groupLink} -> + when (groupRegStatus == GRSActive || pendingApproval groupRegStatus) $ do + let link = ACL SCMContact $ CLShort groupLink + sendChatCmd cc (APIConnectPlan userId (Just link) True Nothing) >>= \case + Right (CRConnectionPlan _ _ (CPGroupLink (GLPKnown {groupInfo = g', groupUpdated = BoolDef updated, linkOwners = ListDef owners}))) -> + checkValidOwner dbOwnerMemberId owners $ do + when updated $ reapprove pg gr groupRegStatus g' + when (updated || summary /= groupSummary g') $ listingsUpdated env + Left (ChatErrorAgent {agentError = SMP _ err}) | linkDeleted err -> + setGroupStatus logError st env cc groupId GRSRemoved $ \gr' -> + notifyOwner gr' "The channel link is no longer valid.\nThe channel is removed from the directory." + _ -> pure () + where + linkDeleted = \case + AUTH -> True + BLOCKED {} -> True + _ -> False + checkValidOwner dbOwnerMemberId owners onValid = case dbOwnerMemberId of + Just ownerGMId -> + withDB "checkGroupLink" cc (\db -> withExceptT show $ getGroupMember db (vr cc) user groupId ownerGMId) >>= \case + Right GroupMember {memberId, memberPubKey} + | any (\GroupLinkOwner {memberId = mId, memberKey} -> memberId == mId && memberPubKey == Just memberKey) owners -> onValid + _ -> setGroupStatus logError st env cc groupId GRSSuspendedBadRoles $ \gr' -> + notifyOwner gr' "The registration owner is no longer a channel owner.\nThe channel is no longer listed in the directory." + Nothing -> onValid + reapprove pg gr groupRegStatus g' = do + let gt = groupTypeStr' pg + groupRef = groupReference gInfo + notifyAdminUsers $ "The " <> gt <> " " <> groupRef <> " profile changed." + case groupRegStatus of + GRSActive -> + setGroupStatus notifyAdminUsers st env cc groupId (GRSPendingApproval 1) $ \gr' -> do + notifyOwner gr' $ "The " <> gt <> " profile has changed.\nIt is hidden from the directory until approved." + sendToApprove g' gr' 1 + GRSPendingApproval n -> + sendToApprove g' gr (n + 1) + _ -> pure () + deContactRoleChanged :: GroupInfo -> ContactId -> GroupMemberRole -> IO () deContactRoleChanged g@GroupInfo {groupId, membership = GroupMember {memberRole = serviceRole}} ctId contactRole = do logInfo $ "contact ID " <> tshow ctId <> " role changed in group " <> viewGroupName g <> " to " <> tshow contactRole @@ -771,63 +888,205 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName >>= mapM_ (\cm@GroupMember {memberRole} -> when (memberRole == GROwner && memberActive cm) action) deContactRemovedFromGroup :: ContactId -> GroupInfo -> IO () - deContactRemovedFromGroup ctId g@GroupInfo {groupId} = do + deContactRemovedFromGroup ctId g@GroupInfo {groupId, groupProfile = GroupProfile {publicGroup = pg_}} = do + let gt = maybe "group" groupTypeStr' pg_ logInfo $ "contact ID " <> tshow ctId <> " removed from group " <> viewGroupName g - withGroupReg g "contact removed" $ \gr -> do + withGroupReg g "contact removed" $ \gr -> when (ctId `isOwner` gr) $ setGroupStatus notifyAdminUsers st env cc groupId GRSRemoved $ \gr' -> do - notifyOwner gr' $ "You are removed from the group " <> userGroupReference gr' g <> ".\n\nThe group is no longer listed in the directory." - notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)." + notifyOwner gr' $ "You are removed from the " <> gt <> " " <> userGroupReference gr' g <> ".\n\nThe " <> gt <> " is no longer listed in the directory." + notifyAdminUsers $ "The " <> gt <> " " <> groupReference g <> " is de-listed (" <> gt <> " owner is removed)." + when (isJust pg_) $ leavePublicGroup g deContactLeftGroup :: ContactId -> GroupInfo -> IO () - deContactLeftGroup ctId g@GroupInfo {groupId} = do + deContactLeftGroup ctId g@GroupInfo {groupId, groupProfile = GroupProfile {publicGroup = pg_}} = do + let gt = maybe "group" groupTypeStr' pg_ logInfo $ "contact ID " <> tshow ctId <> " left group " <> viewGroupName g - -- TODO combine withGroupReg g "contact left" $ \gr -> when (ctId `isOwner` gr) $ setGroupStatus notifyAdminUsers st env cc groupId GRSRemoved $ \gr' -> do - notifyOwner gr' $ "You left the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." - notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)." + notifyOwner gr' $ "You left the " <> gt <> " " <> userGroupReference gr' g <> ".\n\nThe " <> gt <> " is no longer listed in the directory." + notifyAdminUsers $ "The " <> gt <> " " <> groupReference g <> " is de-listed (" <> gt <> " owner left)." + when (isJust pg_) $ leavePublicGroup g deServiceRemovedFromGroup :: GroupInfo -> IO () - deServiceRemovedFromGroup g@GroupInfo {groupId} = do + deServiceRemovedFromGroup g@GroupInfo {groupId, groupProfile = GroupProfile {publicGroup = pg_}} = do + let gt = maybe "group" groupTypeStr' pg_ logInfo $ "service removed from group " <> viewGroupName g setGroupStatus notifyAdminUsers st env cc groupId GRSRemoved $ \gr -> do - notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." - notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)." + notifyOwner gr $ serviceName <> " is removed from the " <> gt <> " " <> userGroupReference gr g <> ".\n\nThe " <> gt <> " is no longer listed in the directory." + notifyAdminUsers $ "The " <> gt <> " " <> groupReference g <> " is de-listed (directory service is removed)." deGroupDeleted :: GroupInfo -> IO () - deGroupDeleted g@GroupInfo {groupId} = do + deGroupDeleted g@GroupInfo {groupId, groupProfile = GroupProfile {publicGroup = pg_}} = do + let gt = maybe "group" groupTypeStr' pg_ logInfo $ "group removed " <> viewGroupName g setGroupStatus notifyAdminUsers st env cc groupId GRSRemoved $ \gr -> do - notifyOwner gr $ "The group " <> userGroupReference gr g <> " is deleted.\n\nThe group is no longer listed in the directory." - notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group is deleted)." + notifyOwner gr $ "The " <> gt <> " " <> userGroupReference gr g <> " is deleted.\n\nThe " <> gt <> " is no longer listed in the directory." + notifyAdminUsers $ "The " <> gt <> " " <> groupReference g <> " is de-listed (" <> gt <> " is deleted)." + + deChatLinkReceived :: Contact -> MsgChatLink -> Maybe LinkOwnerSig -> IO () + deChatLinkReceived ct (MCLGroup {connLink, groupProfile = GroupProfile {publicGroup = Just PublicGroupProfile {groupType}}}) (Just ownerSig@LinkOwnerSig {ownerId = Just (B64UrlByteString oIdBytes)}) = + case groupType of + GTUnknown tag -> sendMessage cc ct $ "Unsupported group type: " <> T.pack (show tag) + gt -> do + let link = ACL SCMContact $ CLShort connLink + mId = MemberId oIdBytes + gt' = groupTypeStr gt + sendChatCmd cc (APIConnectPlan userId (Just link) True (Just ownerSig)) >>= \case + Right (CRConnectionPlan _ (ACCL SCMContact ccLink) plan) -> + handleGroupLinkPlan ct ccLink mId ownerSig gt' plan + _ -> sendMessage cc ct "Error: could not connect. Please report it to directory admins." + deChatLinkReceived ct (MCLGroup {groupProfile = GroupProfile {publicGroup = Just pg}}) _ = + sendMessage cc ct $ "To add a " <> groupTypeStr' pg <> " to directory you must be the owner." + deChatLinkReceived ct _ _ = + sendMessage cc ct "Only channels can be added to directory via link." + + groupTypeStr :: GroupType -> Text + groupTypeStr = \case + GTChannel -> "channel" + GTGroup -> "group" + GTUnknown _ -> "group" + + groupTypeStr' :: PublicGroupProfile -> Text + groupTypeStr' PublicGroupProfile {groupType} = groupTypeStr groupType + + leavePublicGroup :: GroupInfo -> IO () + leavePublicGroup GroupInfo {groupId} = + void $ sendChatCmd cc (APILeaveGroup groupId) + + handleGroupLinkPlan :: Contact -> CreatedLinkContact -> MemberId -> LinkOwnerSig -> Text -> ConnectionPlan -> IO () + handleGroupLinkPlan ct ccLink mId ownerSig gt = \case + CPGroupLink glp -> case glp of + GLPOk {groupSLinkData_, ownerVerification} -> case (groupSLinkData_, ownerVerification) of + (Just groupSLinkData, Just OVVerified) -> joinAndRegisterPublicGroup ct ccLink mId gt groupSLinkData + (_, Just (OVFailed reason)) -> sendMessage cc ct $ "Link signature verification failed: " <> reason <> ".\nYou must be the " <> gt <> " owner to register it." + (Nothing, _) -> sendMessage cc ct $ "Error: no " <> gt <> " information available via the link." + _ -> sendMessage cc ct $ "Error: could not verify " <> gt <> " ownership. Please report it to directory admins." + GLPKnown {groupInfo = g, groupUpdated = BoolDef updated, ownerVerification} -> case ownerVerification of + Just OVVerified -> deReregistration ct g updated ownerSig + Just (OVFailed reason) -> sendMessage cc ct $ "Link signature verification failed: " <> reason <> ".\nYou must be the " <> gt <> " owner to register it." + Nothing -> sendMessage cc ct $ "Error: could not verify " <> gt <> " ownership." + GLPConnectingProhibit _ -> sendMessage cc ct $ "Already connecting to this " <> gt <> "." + GLPConnectingConfirmReconnect -> sendMessage cc ct $ "Already connecting to this " <> gt <> "." + GLPNoRelays _ -> sendMessage cc ct $ T.toTitle gt <> " has no active relays. Please try again later." + GLPOwnLink _ -> sendMessage cc ct "Unexpected error. Please report it to directory admins." + _ -> sendMessage cc ct "Unexpected error. Please report it to directory admins." + + joinAndRegisterPublicGroup :: Contact -> CreatedLinkContact -> MemberId -> Text -> GroupShortLinkData -> IO () + joinAndRegisterPublicGroup ct ccLink mId gt groupSLinkData = do + let GroupShortLinkData {groupProfile = GroupProfile {displayName}} = groupSLinkData + ownerContact = GroupOwnerContact {contactId = contactId' ct, memberId = mId} + sendMessage cc ct $ "Joining the " <> gt <> " " <> displayName <> "…" + sendChatCmd cc (APIPrepareGroup userId ccLink False groupSLinkData) >>= \case + Right (CRNewPreparedChat _ (AChat SCTGroup (Chat (GroupChat gInfo _) _ _))) -> do + let gId = groupId' gInfo + addGroupReg notifyAdminUsers st cc ct gInfo GRSProposed $ \_ -> pure () + sendChatCmd cc (APIConnectPreparedGroup gId False (Just ownerContact) Nothing) >>= \case + Right CRStartedConnectionToGroup {groupInfo = gInfo'} -> + withDB "getGroupMember" cc (\db -> withExceptT show $ getGroupMemberByMemberId db (vr cc) user gInfo' mId) >>= \case + Right ownerMember -> + void $ setGroupRegOwner cc gId ownerMember + Left e -> do + logError $ "could not find owner member: " <> T.pack e + sendMessage cc ct "Error: could not find owner member after joining. Please report it to directory admins." + _ -> sendMessage cc ct $ "Error joining " <> gt <> " " <> displayName <> ", please re-send the link!" + _ -> sendMessage cc ct $ "Error joining " <> gt <> " " <> displayName <> ", please re-send the link!" + + deReregistration :: Contact -> GroupInfo -> Bool -> LinkOwnerSig -> IO () + deReregistration ct g@GroupInfo {groupId, groupProfile = GroupProfile {publicGroup = pg_}} profileChanged LinkOwnerSig {ownerId = Just (B64UrlByteString oIdBytes)} = do + let mId = MemberId oIdBytes + gt = maybe "group" groupTypeStr' pg_ + withDB "getGroupMemberByMemberId" cc (\db -> withExceptT show $ getGroupMemberByMemberId db (vr cc) user g mId) >>= \case + Right ownerMember@GroupMember {memberRole = role, memberStatus} -> + if + | role >= GROwner && memberStatus /= GSMemUnknown -> + getGroupReg cc groupId >>= \case + Right gr + | contactId' ct `isOwner` gr -> sameOwnerReregistration gr gt + | otherwise -> sendMessage cc ct $ "This " <> gt <> " is registered by another owner." + Left _ -> + addGroupReg notifyAdminUsers st cc ct g (GRSPendingApproval 1) $ \gr -> do + void $ setGroupRegOwner cc groupId ownerMember + sendToApprove g gr 1 + | role < GROwner -> sendMessage cc ct $ "You must be the " <> gt <> " owner to register it." + | otherwise -> sendMessage cc ct $ "Waiting for the owner member to be connected to the " <> gt <> "." + Left _ -> sendMessage cc ct $ "Error: could not verify " <> gt <> " ownership. Please report it to directory admins." + where + sameOwnerReregistration gr gt = case groupRegStatus gr of + GRSProposed -> sendMessage cc ct $ "Registration is in progress, waiting for the owner member to be connected to the " <> gt <> "." + GRSPendingConfirmation -> pendingApprovalTransition gr gt 1 + GRSPendingUpdate -> pendingApprovalTransition gr gt 1 + GRSPendingApproval n + | profileChanged -> pendingApprovalTransition gr gt $ n + 1 + | otherwise -> sendMessage cc ct $ T.toTitle gt <> " is already pending approval." + GRSActive + | profileChanged -> pendingApprovalTransition gr gt 1 + | otherwise -> sendMessage cc ct $ T.toTitle gt <> " is already listed in the directory." + GRSSuspended -> sendMessage cc ct $ T.toTitle gt <> " is suspended by admin. Please contact support." + GRSSuspendedBadRoles -> pendingApprovalTransition gr gt 1 + GRSRemoved -> pendingApprovalTransition gr gt 1 + pendingApprovalTransition gr gt n = do + let userGroupRef = userGroupReference gr g + setGroupStatus notifyAdminUsers st env cc groupId (GRSPendingApproval n) $ \gr' -> do + notifyOwner gr' $ + "The " <> gt <> " " <> userGroupRef <> " is submitted for approval.\nIt is hidden from the directory until approved." + sendToApprove g gr' n + deReregistration ct _ _ _ = + sendMessage cc ct "Error: could not verify ownership. Please report it to directory admins." + + deMemberUpdated :: GroupInfo -> GroupMember -> GroupMember -> IO () + deMemberUpdated g@GroupInfo {groupId, groupProfile = GroupProfile {displayName, publicGroup}} fromMember toMember = + withGroupReg g "owner member announced" $ \gr@GroupReg {groupRegStatus, dbOwnerMemberId} -> + when (groupRegStatus == GRSProposed && (dbOwnerMemberId == Just (groupMemberId' fromMember) || dbOwnerMemberId == Just (groupMemberId' toMember))) $ + let GroupMember {memberRole = role} = toMember + gt = maybe "group" groupTypeStr' publicGroup + in if role >= GROwner + then setGroupStatus notifyAdminUsers st env cc groupId (GRSPendingApproval 1) $ \gr' -> do + notifyOwner gr' $ "Joined the " <> gt <> " " <> displayName <> ". Registration is pending approval — it may take up to 48 hours." + sendToApprove g gr' 1 + else do + setGroupStatus notifyAdminUsers st env cc groupId GRSRemoved $ \_ -> pure () + sendMessage' cc (dbContactId gr) "The signing key does not belong to a current owner. Registration cancelled." deUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO () deUserCommand ct ciId = \case DCHelp DHSRegistration -> sendMessage cc ct $ - "You must be the group owner to add it to the directory:\n\n\ - \1️⃣ *Invite* " + "You must be the group or channel owner to add it to the directory.\n\n\ + \*To register a channel*, use _Share via chat_ to send its link to " + <> serviceName + <> " bot.\n\n\ + \*To register a group*:\n\ + \1️⃣ *Invite* " <> serviceName <> " bot to your group as *admin* - it will create a link for new members to join.\n\ - \2️⃣ *Add* this link to the group's welcome message.\n\ - \3️⃣ We *review* your group. Once *approved*, anybody can find it.\n\n\ - \_We usually approve within a day, except holidays_. [More details](https://simplex.chat/docs/directory.html#adding-groups-to-the-directory)." + \2️⃣ *Add* this link to the group's welcome message.\n\n\ + \Once your group or channel *approved*, it can be found here or at [simplex.chat/directory](https://simplex.chat/directory).\n\n\ + \_We usually review within a day, except holidays_. [More details](https://simplex.chat/docs/directory.html#adding-groups-to-the-directory)." DCHelp DHSCommands -> sendMessage cc ct $ "/'help commands' - receive this help message.\n\ - \/help - how to register your group to be added to directory.\n\ + \/help - how to register your group or channel to be added to directory.\n\ \/list - list the groups you registered.\n\ \`/role ` - view and set default member role for your group.\n\ \`/filter ` - view and set spam filter settings for group.\n\ \`/link ` - view and upgrade group link.\n\ \`/delete :` - remove the group you submitted from directory, with _ID_ and _name_ as shown by /list command.\n\n\ \To search for groups, send the search text." - DCSearchGroup s -> - sendFoundListedGroups (STSearch s) Nothing "No groups found" $ \gs n -> -- $ sendSearchResults s + DCSearchGroup s ft -> + sendFoundListedGroups (STSearch s) Nothing notFound $ \gs n -> let more = if n > length gs then ", sending top " <> tshow (length gs) else "" in "Found " <> tshow n <> " group(s)" <> more <> "." + where + notFound + | hasSimplexGroupLink ft = "No groups found.\nTo register a group or a channel, please use \"Share via chat\" feature." + | otherwise = "No groups found" + hasSimplexGroupLink = \case + Just fts -> any isGroupLink fts + Nothing -> False + isGroupLink (FormattedText (Just SimplexLink {linkType}) _) = linkType == XLGroup || linkType == XLChannel + isGroupLink _ = False DCSearchNext -> atomically (TM.lookup (contactId' ct) searchRequests) >>= \case Just SearchRequest {searchType, searchTime, lastGroup} -> do @@ -858,14 +1117,17 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Left e -> sendReply $ "Error reading groups: " <> T.pack e Right gs -> sendGroupsInfo ct ciId isAdmin (gs, length gs) DCDeleteGroup gId gName -> - (if isAdmin then withGroupAndReg sendReply else withUserGroupReg) gId gName $ \GroupInfo {groupProfile = GroupProfile {displayName}} GroupReg {dbGroupId} -> do + (if isAdmin then withGroupAndReg sendReply else withUserGroupReg) gId gName $ \g@GroupInfo {groupProfile = GroupProfile {displayName, publicGroup = pg_}} GroupReg {dbGroupId} -> do + let gt = maybe "group" groupTypeStr' pg_ delGroupReg cc dbGroupId >>= \case Right () -> do logGDelete st dbGroupId - sendReply $ (if isAdmin then "The group " else "Your group ") <> displayName <> " is deleted from the directory" - Left e -> sendReply $ "Error deleting group " <> displayName <> ": " <> T.pack e + sendReply $ (if isAdmin then "The " <> gt <> " " else "Your " <> gt <> " ") <> displayName <> " is deleted from the directory" + when (isJust pg_) $ leavePublicGroup g + Left e -> sendReply $ "Error deleting " <> gt <> " " <> displayName <> ": " <> T.pack e DCMemberRole gId gName_ mRole_ -> - (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do + (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> + ifPublicGroup g (sendReply "This command is not available for public groups.") $ do let GroupInfo {groupProfile = GroupProfile {displayName = n}} = g case mRole_ of Nothing -> @@ -885,7 +1147,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> textEncode mRole <> "*\n" onlyViaLink gLink = "*Please note*: it applies only to members joining via this link: " <> groupLinkText gLink DCGroupFilter gId gName_ acceptance_ -> - (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do + (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> + ifPublicGroup g (sendReply "This command is not available for public groups.") $ do let GroupInfo {groupProfile = GroupProfile {displayName = n}} = g a = groupMemberAcceptance g case acceptance_ of @@ -916,39 +1179,42 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Just PCAll -> "_enabled_" Just PCNoImage -> "_enabled for profiles without image_" DCShowUpgradeGroupLink gId gName_ -> - (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \GroupInfo {groupId, localDisplayName = gName} _ -> do - let groupRef = groupReference' gId gName - withGroupLinkResult groupRef (sendChatCmd cc $ APIGetGroupLink groupId) $ - \GroupLink {connLinkContact = gLink@(CCLink _ sLnk_), acceptMemberRole, shortLinkDataSet, shortLinkLargeDataSet = BoolDef slLargeDataSet} -> do - let shouldBeUpgraded = isNothing sLnk_ || not shortLinkDataSet || not slLargeDataSet - sendReply $ - T.unlines $ - [ "The link to join the group " <> groupRef <> ":", - groupLinkText gLink, - "New member role: " <> textEncode acceptMemberRole - ] - <> ["The link is being upgraded..." | shouldBeUpgraded] - when shouldBeUpgraded $ do - let send = sendComposedMessage cc ct Nothing . MCText . T.unlines - withGroupLinkResult groupRef (sendChatCmd cc $ APIAddGroupShortLink groupId) $ - \GroupLink {connLinkContact = CCLink _ sLnk_'} -> case (sLnk_, sLnk_') of - (Just _, Just _) -> - send ["The group link is upgraded for: " <> groupRef, "No changes to group needed."] - (Nothing, Just sLnk) -> - sendComposedMessages - cc - (SRDirect $ contactId' ct) - [ MCText $ - T.unlines - [ "Please replace the old link in welcome message of your group " <> groupRef, - "If this is the only change, the group will remain listed in directory without re-approval.", - "", - "The new link:" - ], - MCText $ strEncodeTxt sLnk - ] - (_, Nothing) -> - send ["The short link is not created for " <> groupRef, "Please report it to the developers."] + (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \GroupInfo {groupId, groupProfile = GroupProfile {publicGroup = pg_}, localDisplayName = gName} _ -> case pg_ of + Just pg@PublicGroupProfile {groupLink} -> + sendReply $ "The link to join the " <> groupTypeStr' pg <> " " <> groupReference' gId gName <> ":\n" <> strEncodeTxt groupLink + Nothing -> do + let groupRef = groupReference' gId gName + withGroupLinkResult groupRef (sendChatCmd cc $ APIGetGroupLink groupId) $ + \GroupLink {connLinkContact = gLink@(CCLink _ sLnk_), acceptMemberRole, shortLinkDataSet, shortLinkLargeDataSet = BoolDef slLargeDataSet} -> do + let shouldBeUpgraded = isNothing sLnk_ || not shortLinkDataSet || not slLargeDataSet + sendReply $ + T.unlines $ + [ "The link to join the group " <> groupRef <> ":", + groupLinkText gLink, + "New member role: " <> textEncode acceptMemberRole + ] + <> ["The link is being upgraded..." | shouldBeUpgraded] + when shouldBeUpgraded $ do + let send = sendComposedMessage cc ct Nothing . MCText . T.unlines + withGroupLinkResult groupRef (sendChatCmd cc $ APIAddGroupShortLink groupId) $ + \GroupLink {connLinkContact = CCLink _ sLnk_'} -> case (sLnk_, sLnk_') of + (Just _, Just _) -> + send ["The group link is upgraded for: " <> groupRef, "No changes to group needed."] + (Nothing, Just sLnk) -> + sendComposedMessages + cc + (SRDirect $ contactId' ct) + [ MCText $ + T.unlines + [ "Please replace the old link in welcome message of your group " <> groupRef, + "If this is the only change, the group will remain listed in directory without re-approval.", + "", + "The new link:" + ], + MCText $ strEncodeTxt sLnk + ] + (_, Nothing) -> + send ["The short link is not created for " <> groupRef, "Please report it to the developers."] where withGroupLinkResult groupRef a cb = a >>= \case @@ -1000,8 +1266,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName where msgs = replyMsg :| map foundGroup gs <> [moreMsg | moreGroups > 0] replyMsg = (Just ciId, MCText reply) - foundGroup (GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_, memberAdmission}, groupSummary = GroupSummary {currentMembers}}, _) = - let membersStr = "_" <> tshow currentMembers <> " members_" + foundGroup (GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_, memberAdmission}, groupSummary}, _) = + let membersStr = "_" <> membersCountStr p groupSummary <> "_" showId = if isAdmin then tshow groupId <> ". " else "" text = T.unlines $ [showId <> groupInfoText p, membersStr] ++ knockingStr memberAdmission in (Nothing, maybe (MCText text) (\image -> MCImage {text, image}) image_) @@ -1014,40 +1280,49 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName withGroupAndReg sendReply groupId n $ \g gr@GroupReg {userGroupRegId = ugrId, promoted} -> case groupRegStatus gr of GRSPendingApproval gaId - | gaId == groupApprovalId -> + | gaId == groupApprovalId -> do + let GroupInfo {groupProfile = GroupProfile {publicGroup = pg_}} = g + isPublicGroup_ = isJust pg_ + gt = maybe "group" groupTypeStr' pg_ getDuplicateGroup g >>= \case Left e -> sendReply $ "Error: getDuplicateGroup. Please notify the developers.\n" <> T.pack e - Right DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory." - _ -> getGroupRolesStatus g gr >>= \case - Right GRSOk -> do - let grPromoted' - | promoted || knownCt `elem` superUsers = fromMaybe promoted promote - | otherwise = False - setGroupStatusPromo sendReply st env cc gr GRSActive grPromoted' $ do - let approved = "The group " <> userGroupReference' gr n <> " is approved" - notifyOwner gr $ - (approved <> " and listed in directory - please moderate it!\n") - <> "_Please note_: if you change the group profile it will be hidden from directory until it is re-approved.\n\n" - <> "Supported commands:\n" - <> ("/'filter " <> tshow ugrId <> "' - to configure anti-spam filter.\n") - <> ("/'role " <> tshow ugrId <> "' - to set default member role.\n") - <> ("/'link " <> tshow ugrId <> "' - to view/upgrade group link.") - invited <- - forM ownersGroup $ \og@KnownGroup {localDisplayName = ogName} -> do - inviteToOwnersGroup og gr $ \case - Right () -> do - owner <- groupOwnerInfo groupRef $ dbContactId gr - pure $ "Invited " <> owner <> " to owners' group " <> viewName ogName - Left err -> pure err - sendReply $ "Group approved" <> (if grPromoted' then " (promoted)" else "") <> "!" <> maybe "" ("\n" <>) invited - notifyOtherSuperUsers $ approved <> " by " <> viewName (localDisplayName' ct) <> maybe "" ("\n" <>) invited - Right GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin - Right GRSContactNotOwner -> replyNotApproved "user is not an owner." - Right GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin - Left e -> sendReply $ "Error: getGroupRolesStatus. Please notify the developers.\n" <> T.pack e - where - replyNotApproved reason = sendReply $ "Group is not approved: " <> reason - serviceNotAdmin = serviceName <> " is not an admin." + Right DGReserved -> sendReply $ "The " <> gt <> " " <> groupRef <> " is already listed in the directory." + _ -> do + rolesOk <- if isPublicGroup_ then pure (Right GRSOk) else getGroupRolesStatus g gr + case rolesOk of + Right GRSOk -> do + let grPromoted' + | promoted || knownCt `elem` superUsers = fromMaybe promoted promote + | otherwise = False + setGroupStatusPromo sendReply st env cc gr GRSActive grPromoted' $ do + let approved = "The " <> gt <> " " <> userGroupReference' gr n <> " is approved" + let commands + | isPublicGroup_ = "" + | otherwise = + "\n\nSupported commands:\n" + <> ("/'filter " <> tshow ugrId <> "' - to configure anti-spam filter.\n") + <> ("/'role " <> tshow ugrId <> "' - to set default member role.\n") + <> ("/'link " <> tshow ugrId <> "' - to view/upgrade group link.") + notifyOwner gr $ + (approved <> " and listed in directory - please moderate it!\n") + <> "_Please note_: if you change the " <> gt <> " profile it will be hidden from directory until it is re-approved." + <> commands + invited <- + forM ownersGroup $ \og@KnownGroup {localDisplayName = ogName} -> do + inviteToOwnersGroup og gr $ \case + Right () -> do + owner <- groupOwnerInfo groupRef $ dbContactId gr + pure $ "Invited " <> owner <> " to owners' group " <> viewName ogName + Left err -> pure err + sendReply $ T.toTitle gt <> " approved" <> (if grPromoted' then " (promoted)" else "") <> "!" <> maybe "" ("\n" <>) invited + notifyOtherSuperUsers $ approved <> " by " <> viewName (localDisplayName' ct) <> maybe "" ("\n" <>) invited + Right GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin + Right GRSContactNotOwner -> replyNotApproved "user is not an owner." + Right GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin + Left e -> sendReply $ "Error: getGroupRolesStatus. Please notify the developers.\n" <> T.pack e + 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 @@ -1120,7 +1395,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName r -> contErr r r -> contErr r where - alreadyMember = isJust . find ((Just ctId ==) . memberContactId) + alreadyMember = any (\m -> memberContactId m == Just ctId && memberCurrent m) contErr r = do let err = "error inviting contact ID " <> tshow ctId <> " to owners' group: " <> tshow r putStrLn $ T.unpack err @@ -1189,7 +1464,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName GroupReg {userGroupRegId, groupRegStatus} = gr useGroupId = if isAdmin then groupId else userGroupRegId statusStr = "Status: " <> groupRegStatusText groupRegStatus - membersStr = "_" <> tshow (currentMembers groupSummary) <> " members_" + membersStr = "_" <> membersCountStr p groupSummary <> "_" cmds = "/'role " <> tshow useGroupId <> "', /'filter " <> tshow useGroupId <> "'" ownerStr = maybe "" (("Owner: " <>) . either (("getContact error: " <>) . T.pack) localDisplayName') ct_ text = T.unlines $ [tshow useGroupId <> ". " <> groupInfoText p] ++ [ownerStr | isAdmin] ++ [membersStr, statusStr] ++ knockingStr memberAdmission ++ [cmds] @@ -1203,7 +1478,7 @@ setGroupStatusPromo sendReply st env cc GroupReg {dbGroupId = gId} grStatus' grP Left e -> sendReply $ "Error updating group " <> tshow gId <> " status: " <> T.pack e Right (status, grPromoted) -> do when ((status == DSListed || status' == DSListed) && (status /= status' || grPromoted /= grPromoted')) $ - listingsUpdated env cc + listingsUpdated env logGUpdateStatus st gId grStatus' logGUpdatePromotion st gId grPromoted' continue @@ -1223,7 +1498,7 @@ setGroupStatus sendMsg st env cc gId grStatus' continue = do Left e -> sendMsg $ "Error updating group " <> tshow gId <> " status: " <> T.pack e Right (grStatus, gr) -> do let status = grDirectoryStatus grStatus - when ((status == DSListed || status' == DSListed) && status /= status') $ listingsUpdated env cc + when ((status == DSListed || status' == DSListed) && status /= status') $ listingsUpdated env logGUpdateStatus st gId grStatus' continue gr @@ -1232,7 +1507,7 @@ setGroupPromoted sendReply st env cc GroupReg {dbGroupId = gId} grPromoted' cont setGroupPromotedStore cc gId grPromoted' >>= \case Left e -> sendReply $ "Error updating group " <> tshow gId <> " status: " <> T.pack e Right (status, grPromoted) -> do - when (status == DSListed && grPromoted' /= grPromoted) $ listingsUpdated env cc + when (status == DSListed && grPromoted' /= grPromoted) $ listingsUpdated env logGUpdatePromotion st gId grPromoted' continue diff --git a/apps/simplex-support-bot/.gitignore b/apps/simplex-support-bot/.gitignore new file mode 100644 index 0000000000..9f77d70eda --- /dev/null +++ b/apps/simplex-support-bot/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +data/ +.env diff --git a/apps/simplex-support-bot/README.md b/apps/simplex-support-bot/README.md new file mode 100644 index 0000000000..19b9ab8bd6 --- /dev/null +++ b/apps/simplex-support-bot/README.md @@ -0,0 +1,101 @@ +# SimpleX Support Bot + +A business-address bot that triages incoming support chats, optionally runs them through Grok, and routes handoffs to a team group. + +## Prerequisites + +- Node.js v18 or newer (v24 tested) +- `GROK_API_KEY` env var (xAI) — optional; the bot runs without it +- For the PostgreSQL backend: Linux x86_64, `libpq5` installed on the host, and a reachable PostgreSQL server + +## Install & build + +```bash +cd apps/simplex-support-bot +npm install # downloads native libs + transitive deps +npm run build # tsc +``` + +By default this installs the **SQLite** backend. + +To use **PostgreSQL** instead, drop a `.npmrc` next to `package.json` *before* `npm install`: + +```bash +echo 'simplex_backend=postgres' > .npmrc +npm install # now pulls postgres-flavored native libs +npm run build +``` + +`.npmrc` lives next to the package — npm reads it natively, no extra setup. + +### Switching backends + +`npm install` is a no-op for already-installed deps, so editing `.npmrc` and re-running `npm install` will *not* re-trigger `simplex-chat`'s preinstall. To switch backends, force a clean install: + +```bash +rm -rf node_modules +npm install # download-libs.js re-runs and pulls the right native lib +``` + +## Run + +```bash +mkdir -p data # state file lives here by default + +# SQLite (default) +npm start -- --team-group "Support Team" + +# PostgreSQL +npm start -- --team-group "Support Team" \ + --pg-conn "postgres://user:pass@host/db" +``` + +The bot runs via `npm start` so npm can expose `.npmrc` settings to the process — `detectBackend()` reads `npm_config_simplex_backend` to know which backend was installed. + +## Flags + +Run `npm start -- --help` for the auto-generated reference. Summary: + +| Flag | Backend | Required | Default | Description | +|---|---|---|---|---| +| `--team-group` | both | yes | — | team group display name | +| `--state-file` | both | no | `./data/state.json` | path to bot state JSON | +| `--sqlite-file-prefix` | sqlite | no | `./data/simplex` | DB file prefix (creates `_chat.db`, `_agent.db`) | +| `--sqlite-key` | sqlite | no | (unencrypted) | SQLCipher encryption key | +| `--pg-conn` | postgres | yes | — | PostgreSQL connection string | +| `--pg-schema` | postgres | no | `simplex_v1` | schema prefix used for bot tables | +| `-a` / `--auto-add-team-members` | both | no | | comma-separated `ID:name` pairs (e.g. `1:Alice,2:Bob`) | +| `--timezone` | both | no | `UTC` | IANA zone for weekend detection | +| `--complete-hours` | both | no | `3` | auto-complete chats after N hours idle (`0` disables) | +| `--card-flush-seconds` | both | no | `300` | debounce card state writes | +| `--context-file` | both | required with `GROK_API_KEY` | | text file with Grok system context | +| `-h` / `--help` | both | no | | show usage and exit | + +## Environment variables + +| Var | Purpose | +|---|---| +| `GROK_API_KEY` | xAI API key; enables Grok replies | +| `SIMPLEX_BACKEND` | alternative to `.npmrc` for selecting the install backend (`sqlite` or `postgres`) | + +## Local development against unreleased lib changes + +This package depends on `simplex-chat` from npm. To test against an in-tree version: + +```bash +# In packages/simplex-chat-nodejs +npm link + +# In apps/simplex-support-bot +npm link simplex-chat +``` + +`npm unlink simplex-chat && npm install` reverts to the registry version. + +## Troubleshooting + +- **`--pg-conn is required when backend is postgres`** — the postgres backend is installed but you didn't pass a connection string. +- **`libpq5` errors at startup** — install `libpq5` on the host (`apt install libpq5` on Debian/Ubuntu). +- **`ENOENT: no such file or directory, open './data/state.json'`** — the parent directory of `--state-file` must exist; `mkdir -p data` before starting. +- **Wrong backend installed** — check `node_modules/simplex-chat/libs/installed.txt`. Edit `.npmrc`, then `rm -rf node_modules && npm install` to switch (`npm install` alone won't re-run the dep's preinstall). +- **`libpq` connection error** at startup with sqlite-flavored config (or vice versa) — `.npmrc` was changed but libs weren't reinstalled. See "Switching backends" above. diff --git a/apps/simplex-support-bot/bot.test.ts b/apps/simplex-support-bot/bot.test.ts new file mode 100644 index 0000000000..de787ae4cc --- /dev/null +++ b/apps/simplex-support-bot/bot.test.ts @@ -0,0 +1,2506 @@ +import {describe, test, expect, beforeEach, vi} from "vitest" +import {core} from "simplex-chat" +import {SupportBot} from "./src/bot.js" +import {CardManager} from "./src/cards.js" +import {parseConfig} from "./src/config.js" +import {GrokApiClient} from "./src/grok.js" +import {welcomeMessage, queueMessage, grokActivatedMessage, teamLockedMessage, teamAlreadyInvitedMessage} from "./src/messages.js" + +// Silence console output during tests +vi.spyOn(console, "log").mockImplementation(() => {}) +vi.spyOn(console, "error").mockImplementation(() => {}) + +// ─── Type stubs ─── + +const ChatType = {Direct: "direct" as const, Group: "group" as const, Local: "local" as const} +const GroupMemberRole = {Member: "member" as const, Owner: "owner" as const, Admin: "admin" as const} +const GroupMemberStatus = {Connected: "connected" as const, Complete: "complete" as const, Announced: "announced" as const, Left: "left" as const} +const GroupFeatureEnabled = {On: "on" as const, Off: "off" as const} +const CIDeleteMode = {Broadcast: "broadcast" as const} + +// ─── Mock infrastructure ─── + +let nextItemId = 1000 + +class MockChatApi { + sent: {chat: [string, number]; text: string}[] = [] + added: {groupId: number; contactId: number; role: string}[] = [] + removed: {groupId: number; memberIds: number[]}[] = [] + joined: number[] = [] + deleted: {chatType: string; chatId: number; itemIds: number[]; mode: string}[] = [] + customData = new Map() + roleChanges: {groupId: number; memberIds: number[]; role: string}[] = [] + profileUpdates: {groupId: number; profile: any}[] = [] + + members = new Map() + chatItems = new Map() + groups = new Map() + activeUserId = 1 + + private _addMemberFails = false + private _addMemberError: any = null + private _deleteChatItemsFails = false + + apiAddMemberWillFail(err?: any) { this._addMemberFails = true; this._addMemberError = err } + apiDeleteChatItemsWillFail() { this._deleteChatItemsFails = true } + + async apiSetActiveUser(userId: number) { this.activeUserId = userId; return {userId, profile: {displayName: "test"}} } + async apiSendMessages(chatRef: any, messages: any[]) { + // Normalize chat ref: accept both [type, id] tuples and {chatType, chatId} objects + const chat: [string, number] = Array.isArray(chatRef) + ? chatRef + : [chatRef.chatType, chatRef.chatId] + return messages.map(msg => { + const text = msg.msgContent?.text || "" + this.sent.push({chat, text}) + const itemId = nextItemId++ + return {chatItem: {meta: {itemId}, chatDir: {type: "groupSnd"}, content: {type: "sndMsgContent", msgContent: {type: "text", text}}}} + }) + } + async apiSendTextMessage(chat: [string, number], text: string) { + return this.apiSendMessages(chat, [{msgContent: {type: "text", text}, mentions: {}}]) + } + async apiAddMember(groupId: number, contactId: number, role: string) { + if (this._addMemberFails) { + this._addMemberFails = false + throw this._addMemberError || new Error("apiAddMember failed") + } + this.added.push({groupId, contactId, role}) + const memberId = `member-${contactId}` + const groupMemberId = 5000 + contactId + return {memberId, groupMemberId, memberContactId: contactId, memberStatus: GroupMemberStatus.Connected, memberProfile: {displayName: `Contact${contactId}`}} + } + async apiRemoveMembers(groupId: number, memberIds: number[]) { + this.removed.push({groupId, memberIds}) + return memberIds.map(id => ({groupMemberId: id})) + } + async apiJoinGroup(groupId: number) { + this.joined.push(groupId) + return {groupId} + } + async apiSetMembersRole(groupId: number, memberIds: number[], role: string) { + this.roleChanges.push({groupId, memberIds, role}) + } + async apiListMembers(groupId: number) { + return this.members.get(groupId) || [] + } + async apiGetChat(chatType: string, chatId: number, _count: number) { + if (chatType === ChatType.Direct) { + // Tests don't exercise direct lookups; throw the same shape production + // would so getContact() resolves to null instead of synthesizing a contact. + throw new core.ChatAPIError("contact not found", { + type: "errorStore", + storeError: {type: "contactNotFound", contactId: chatId}, + } as any) + } + const baseGroupInfo = this.groups.get(chatId) + if (!baseGroupInfo) { + // Mirror production behavior: the real apiGetChat throws "groupNotFound" + // for an unknown id; getGroupInfo() catches and returns null. + throw new core.ChatAPIError("group not found", { + type: "errorStore", + storeError: {type: "groupNotFound", groupId: chatId}, + } as any) + } + const items = this.chatItems.get(chatId) || [] + const groupInfo = {...baseGroupInfo, customData: this.customData.get(chatId)} + return { + chatInfo: {type: "group", groupInfo}, + chatItems: items, + chatStats: {unreadCount: 0, unreadMentions: 0, reportsCount: 0, minUnreadItemId: 0, unreadChat: false}, + } + } + async apiGetChats(_userId: number, _pagination: any, _query?: any, _pcc?: boolean) { + return [...this.groups.values()].map(g => ({ + chatInfo: {type: "group", groupInfo: {...g, customData: this.customData.get(g.groupId)}}, + chatItems: [], + chatStats: {unreadCount: 0, unreadMentions: 0, reportsCount: 0, minUnreadItemId: 0, unreadChat: false}, + })) + } + async apiListGroups(_userId: number) { + return [...this.groups.values()].map(g => ({...g, customData: this.customData.get(g.groupId)})) + } + async apiSetGroupCustomData(groupId: number, data?: any) { + if (data === undefined) this.customData.delete(groupId) + else this.customData.set(groupId, data) + } + async apiDeleteChatItems(chatType: string, chatId: number, itemIds: number[], mode: string) { + if (this._deleteChatItemsFails) { + this._deleteChatItemsFails = false + throw new Error("apiDeleteChatItems failed") + } + this.deleted.push({chatType, chatId, itemIds, mode}) + return [] + } + async apiUpdateGroupProfile(groupId: number, profile: any) { + this.profileUpdates.push({groupId, profile}) + return this.groups.get(groupId) || makeGroupInfo(groupId) + } + + memberContacts: {groupId: number; groupMemberId: number; contactId: number}[] = [] + memberContactInvitations: {contactId: number; text: string}[] = [] + + async apiCreateMemberContact(groupId: number, groupMemberId: number): Promise { + const contactId = nextItemId++ + this.memberContacts.push({groupId, groupMemberId, contactId}) + return {contactId, profile: {displayName: "member"}} + } + async apiSendMemberContactInvitation(contactId: number, message?: any): Promise { + const text = typeof message === "string" ? message : (message?.text ?? "") + this.memberContactInvitations.push({contactId, text}) + this.sent.push({chat: [ChatType.Direct, contactId], text}) + return {contactId, profile: {displayName: "member"}} + } + + rawCmds: string[] = [] + async sendChatCmd(cmd: string) { + this.rawCmds.push(cmd) + return {type: "cmdOk"} + } + + sentTo(groupId: number): string[] { + return this.sent.filter(s => s.chat[0] === ChatType.Group && s.chat[1] === groupId).map(s => s.text) + } + lastSentTo(groupId: number): string | undefined { + const msgs = this.sentTo(groupId) + return msgs[msgs.length - 1] + } + sentDirect(contactId: number): string[] { + return this.sent.filter(s => s.chat[0] === ChatType.Direct && s.chat[1] === contactId).map(s => s.text) + } +} + +class MockGrokApi { + calls: {history: any[]; message: string}[] = [] + private _response = "Grok answer" + private _willFail = false + private _gate: {promise: Promise; release: () => void} | null = null + + willRespond(text: string) { this._response = text; this._willFail = false } + willFail() { this._willFail = true } + + // Block every subsequent chat() call until releaseChat() is invoked. Used to + // observe in-flight concurrency without relying on wall-clock timing. + blockChat() { + let release!: () => void + const promise = new Promise(r => { release = r }) + this._gate = {promise, release} + } + releaseChat() { + this._gate?.release() + this._gate = null + } + + async chat(history: any[], userMessage: string): Promise { + this.calls.push({history, message: userMessage}) + if (this._gate) await this._gate.promise + if (this._willFail) { this._willFail = false; throw new Error("Grok API error") } + return this._response + } +} + +// ─── Factory helpers ─── + +const MAIN_USER_ID = 1 +const GROK_USER_ID = 2 +const TEAM_GROUP_ID = 50 +const CUSTOMER_GROUP_ID = 100 +const GROK_CONTACT_ID = 10 +const TEAM_MEMBER_1_ID = 20 +const TEAM_MEMBER_2_ID = 21 +const GROK_LOCAL_GROUP_ID = 200 +const CUSTOMER_ID = "customer-1" + +// Commands passed into SupportBot; matches what index.ts constructs when +// Grok is enabled. Tests that disable grokApi still pass the full list +// because the ctor doesn't care; the value is pushed to a group's +// groupPreferences on the first sendToGroup() call. +const DESIRED_COMMANDS = [ + {type: "command" as const, keyword: "grok", label: "Ask Grok"}, + {type: "command" as const, keyword: "team", label: "Switch to team"}, +] + +// ─── Member factories ─── + +function makeTeamMember(contactId: number, name = `Contact${contactId}`, groupMemberId?: number) { + return { + memberId: `team-${contactId}`, + groupMemberId: groupMemberId ?? 5000 + contactId, + memberContactId: contactId, + memberStatus: GroupMemberStatus.Connected, + memberProfile: {displayName: name}, + } +} + +function makeGrokMember(groupMemberId = 7777) { + return { + memberId: "grok-member", + groupMemberId, + memberContactId: GROK_CONTACT_ID, + memberStatus: GroupMemberStatus.Connected, + memberProfile: {displayName: "Grok"}, + } +} + +function makeCustomerMember(status = GroupMemberStatus.Connected) { + return { + memberId: CUSTOMER_ID, + groupMemberId: 3000, + memberStatus: status, + memberProfile: {displayName: "Customer"}, + } +} + +function makeConfig(overrides: Partial = {}) { + return { + stateFile: "./test-data/state.json", + db: {type: "sqlite", filePrefix: "./test-data/simplex"}, + teamGroup: {id: TEAM_GROUP_ID, name: "SupportTeam"}, + teamMembers: [ + {id: TEAM_MEMBER_1_ID, name: "Alice"}, + {id: TEAM_MEMBER_2_ID, name: "Bob"}, + ], + groupLinks: "", + timezone: "UTC", + completeHours: 3, + cardFlushSeconds: 300, + grokApiKey: "test-key", + grokContactId: GROK_CONTACT_ID as number | null, + ...overrides, + } +} + +function makeGroupInfo(groupId: number, opts: Partial = {}): any { + return { + groupId, + groupProfile: {displayName: opts.displayName || `Group${groupId}`, fullName: ""}, + businessChat: opts.businessChat !== undefined ? opts.businessChat : { + chatType: "business", + businessId: "bot-1", + customerId: opts.customerId || CUSTOMER_ID, + }, + membership: {memberId: "bot-member"}, + customData: opts.customData, + chatSettings: {enableNtfs: "all", favorite: false}, + fullGroupPreferences: {}, + localDisplayName: `group-${groupId}`, + localAlias: "", + useRelays: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + chatTags: [], + groupSummary: {}, + membersRequireAttention: 0, + } +} + +function makeUser(userId: number) { + return {userId, profile: {displayName: userId === MAIN_USER_ID ? "Ask SimpleX Team" : "Grok"}} +} + +function makeChatItem(opts: { + dir: "groupSnd" | "groupRcv" | "directRcv" + text?: string + memberId?: string + memberContactId?: number + memberDisplayName?: string + msgType?: string + groupId?: number +}): any { + const itemId = nextItemId++ + const now = new Date().toISOString() + const msgContent = opts.msgType + ? {type: opts.msgType, text: opts.text || ""} + : {type: "text", text: opts.text || ""} + + let chatDir: any + if (opts.dir === "groupSnd") { + chatDir = {type: "groupSnd"} + } else if (opts.dir === "groupRcv") { + chatDir = { + type: "groupRcv", + groupMember: { + memberId: opts.memberId || CUSTOMER_ID, + groupMemberId: 3000, + memberContactId: opts.memberContactId, + memberStatus: GroupMemberStatus.Connected, + memberProfile: {displayName: opts.memberDisplayName || "Customer"}, + }, + } + } else { + chatDir = {type: "directRcv"} + } + + return { + chatDir, + meta: {itemId, itemTs: now, createdAt: now, itemText: opts.text || "", itemStatus: {type: "sndSent"}, itemEdited: false}, + content: {type: opts.dir === "groupSnd" ? "sndMsgContent" : "rcvMsgContent", msgContent}, + mentions: {}, + reactions: [], + } +} + +function makeAChatItem(chatItem: any, groupId = CUSTOMER_GROUP_ID): any { + return { + chatInfo: {type: "group", groupInfo: makeGroupInfo(groupId)}, + chatItem, + } +} + +function makeDirectAChatItem(chatItem: any, contactId: number): any { + return { + chatInfo: {type: "direct", contact: {contactId, profile: {displayName: "Someone"}}}, + chatItem, + } +} + +// ─── Shared test state ─── + +let chat: MockChatApi +let grokApi: MockGrokApi +let config: ReturnType +let bot: InstanceType +let cards: InstanceType + +// ─── Setup and helpers ─── + +function setup(configOverrides: Partial = {}) { + nextItemId = 1000 + chat = new MockChatApi() + grokApi = new MockGrokApi() + config = makeConfig(configOverrides) + + // Register team group and customer group in mock + const teamGroupInfo = makeGroupInfo(TEAM_GROUP_ID, {businessChat: null, displayName: "SupportTeam"}) + chat.groups.set(TEAM_GROUP_ID, teamGroupInfo) + chat.groups.set(CUSTOMER_GROUP_ID, makeGroupInfo(CUSTOMER_GROUP_ID)) + + cards = new CardManager(chat as any, config as any, MAIN_USER_ID, 999999999) + bot = new SupportBot(chat as any, grokApi as any, config as any, MAIN_USER_ID, GROK_USER_ID, DESIRED_COMMANDS) + // Replace cards with our constructed one that has a long flush interval + bot.cards = cards +} + +function customerMessage(text: string, groupId = CUSTOMER_GROUP_ID): any { + const ci = makeChatItem({dir: "groupRcv", text, memberId: CUSTOMER_ID}) + return { + type: "newChatItems" as const, + user: makeUser(MAIN_USER_ID), + chatItems: [makeAChatItem(ci, groupId)], + } +} + +function customerNonTextMessage(groupId = CUSTOMER_GROUP_ID): any { + const ci = makeChatItem({dir: "groupRcv", text: "", memberId: CUSTOMER_ID, msgType: "image"}) + return { + type: "newChatItems" as const, + user: makeUser(MAIN_USER_ID), + chatItems: [makeAChatItem(ci, groupId)], + } +} + +function teamMemberMessage(text: string, contactId = TEAM_MEMBER_1_ID, groupId = CUSTOMER_GROUP_ID): any { + const ci = makeChatItem({dir: "groupRcv", text, memberId: `team-${contactId}`, memberContactId: contactId, memberDisplayName: "Alice"}) + return { + type: "newChatItems" as const, + user: makeUser(MAIN_USER_ID), + chatItems: [makeAChatItem(ci, groupId)], + } +} + +function grokResponseMessage(text: string, groupId = CUSTOMER_GROUP_ID): any { + const ci = makeChatItem({dir: "groupRcv", text, memberId: "grok-member", memberContactId: GROK_CONTACT_ID, memberDisplayName: "Grok"}) + return { + type: "newChatItems" as const, + user: makeUser(MAIN_USER_ID), + chatItems: [makeAChatItem(ci, groupId)], + } +} + +function directMessage(text: string, contactId: number): any { + const ci = makeChatItem({dir: "directRcv", text}) + return { + type: "newChatItems" as const, + user: makeUser(MAIN_USER_ID), + chatItems: [makeDirectAChatItem(ci, contactId)], + } +} + +function teamGroupMessage(text: string, senderContactId = TEAM_MEMBER_1_ID): any { + const ci = makeChatItem({dir: "groupRcv", text, memberId: `team-${senderContactId}`, memberContactId: senderContactId, memberDisplayName: "Alice"}) + return { + type: "newChatItems" as const, + user: makeUser(MAIN_USER_ID), + chatItems: [{chatInfo: {type: "group", groupInfo: makeGroupInfo(TEAM_GROUP_ID, {businessChat: null})}, chatItem: ci}], + } +} + +// Simulate bot sending a message to the customer group (adds it to chatItems history) +function addBotMessage(text: string, groupId = CUSTOMER_GROUP_ID) { + const ci = makeChatItem({dir: "groupSnd", text}) + const items = chat.chatItems.get(groupId) || [] + items.push(ci) + chat.chatItems.set(groupId, items) +} + +function addCustomerMessageToHistory(text: string, groupId = CUSTOMER_GROUP_ID) { + const ci = makeChatItem({dir: "groupRcv", text, memberId: CUSTOMER_ID}) + const items = chat.chatItems.get(groupId) || [] + items.push(ci) + chat.chatItems.set(groupId, items) +} + +function addTeamMemberMessageToHistory(text: string, contactId = TEAM_MEMBER_1_ID, groupId = CUSTOMER_GROUP_ID) { + const ci = makeChatItem({dir: "groupRcv", text, memberId: `team-${contactId}`, memberContactId: contactId}) + const items = chat.chatItems.get(groupId) || [] + items.push(ci) + chat.chatItems.set(groupId, items) +} + +function addGrokMessageToHistory(text: string, groupId = CUSTOMER_GROUP_ID) { + const ci = makeChatItem({dir: "groupRcv", text, memberId: "grok-member", memberContactId: GROK_CONTACT_ID}) + const items = chat.chatItems.get(groupId) || [] + items.push(ci) + chat.chatItems.set(groupId, items) +} + +// State helpers — reach specific states +async function reachQueue(groupId = CUSTOMER_GROUP_ID) { + await bot.onNewChatItems(customerMessage("Hello, I need help", groupId)) + // This should have sent queue message + created card +} + +async function reachGrok(groupId = CUSTOMER_GROUP_ID) { + await reachQueue(groupId) + // Add the queue message to history so state derivation sees it + addBotMessage("The team will reply to your message", groupId) + + // Send /grok command. This triggers activateGrok which needs the join flow. + // We need to simulate Grok join success. + const grokJoinPromise = simulateGrokJoinSuccess(groupId) + await bot.onNewChatItems(customerMessage("/grok", groupId)) + await grokJoinPromise +} + +async function simulateGrokJoinSuccess(mainGroupId = CUSTOMER_GROUP_ID) { + // Wait for apiAddMember to be called, then simulate Grok invitation + join + await new Promise(r => setTimeout(r, 10)) + // Find the pending grok join via the added members + const addedGrok = chat.added.find(a => a.contactId === GROK_CONTACT_ID && a.groupId === mainGroupId) + if (!addedGrok) return + + // Simulate Grok receivedGroupInvitation + const memberId = `member-${GROK_CONTACT_ID}` + await bot.onGrokGroupInvitation({ + type: "receivedGroupInvitation", + user: makeUser(GROK_USER_ID), + groupInfo: {...makeGroupInfo(GROK_LOCAL_GROUP_ID), membership: {memberId}}, + contact: {contactId: 99}, + fromMemberRole: GroupMemberRole.Admin, + memberRole: GroupMemberRole.Member, + }) + + // Simulate Grok connectedToGroupMember + await bot.onGrokMemberConnected({ + type: "connectedToGroupMember", + user: makeUser(GROK_USER_ID), + groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID), + member: {memberId: "bot-in-grok-view", groupMemberId: 9999, memberContactId: undefined}, + }) +} + +async function reachTeamPending(groupId = CUSTOMER_GROUP_ID) { + await reachQueue(groupId) + addBotMessage("The team will reply to your message", groupId) + await bot.onNewChatItems(customerMessage("/team", groupId)) +} + +async function reachTeam(groupId = CUSTOMER_GROUP_ID) { + await reachTeamPending(groupId) + addBotMessage("We will reply within 24 hours.", groupId) + chat.members.set(groupId, [makeTeamMember(TEAM_MEMBER_1_ID, "Alice")]) + // Team member sends a text message (triggers one-way gate) + addTeamMemberMessageToHistory("Hi, how can I help?", TEAM_MEMBER_1_ID, groupId) + await bot.onNewChatItems(teamMemberMessage("Hi, how can I help?", TEAM_MEMBER_1_ID, groupId)) +} + +// ─── Assertion helpers ─── + +function expectSentToGroup(groupId: number, substring: string) { + const msgs = chat.sentTo(groupId) + expect(msgs.some(m => m.includes(substring)), + `Expected message containing "${substring}" sent to group ${groupId}, got:\n${msgs.join("\n")}` + ).toBe(true) +} + +function expectNotSentToGroup(groupId: number, substring: string) { + expect(chat.sentTo(groupId).every(m => !m.includes(substring))).toBe(true) +} + +function expectDmSent(contactId: number, substring: string) { + expect(chat.sentDirect(contactId).some(m => m.includes(substring))).toBe(true) +} + +function expectAnySent(substring: string) { + expect(chat.sent.some(s => s.text.includes(substring))).toBe(true) +} + +function expectMemberAdded(groupId: number, contactId: number) { + expect(chat.added.some(a => a.groupId === groupId && a.contactId === contactId)).toBe(true) +} + +function expectCardDeleted(cardItemId: number) { + expect(chat.deleted.some(d => d.itemIds.includes(cardItemId))).toBe(true) +} + +// ─── Event factories ─── + +function connectedEvent(groupId: number, member: any, memberContact?: any) { + return { + type: "connectedToGroupMember" as const, + user: makeUser(MAIN_USER_ID), + groupInfo: makeGroupInfo(groupId, groupId === TEAM_GROUP_ID ? {businessChat: null} : {}), + member, + ...(memberContact !== undefined ? {memberContact} : {}), + } +} + +function leftEvent(groupId: number, member: any) { + return { + type: "leftMember" as const, + user: makeUser(MAIN_USER_ID), + groupInfo: makeGroupInfo(groupId, groupId === TEAM_GROUP_ID ? {businessChat: null} : {}), + member: {...member, memberStatus: GroupMemberStatus.Left}, + } +} + +function updatedEvent(groupId: number, chatItem: any, userId = MAIN_USER_ID) { + return { + type: "chatItemUpdated" as const, + user: makeUser(userId), + chatItem: { + chatInfo: {type: "group", groupInfo: makeGroupInfo(groupId, groupId === TEAM_GROUP_ID ? {businessChat: null} : {})}, + chatItem, + }, + } +} + +function reactionEvent(groupId: number, added: boolean) { + return { + type: "chatItemReaction" as const, + user: makeUser(MAIN_USER_ID), + added, + reaction: { + chatInfo: {type: "group", groupInfo: makeGroupInfo(groupId)}, + chatReaction: {reaction: {type: "emoji", emoji: "👍"}}, + }, + } +} + +function joinedEvent(groupId: number, member: any, userId = MAIN_USER_ID) { + return { + type: "joinedGroupMember" as const, + user: makeUser(userId), + groupInfo: makeGroupInfo(groupId, groupId === TEAM_GROUP_ID ? {businessChat: null} : {}), + member, + } +} + +function grokViewCustomerMessage(text: string, msgType?: string) { + chat.groups.set(GROK_LOCAL_GROUP_ID, makeGroupInfo(GROK_LOCAL_GROUP_ID)) + const ci = makeChatItem({dir: "groupRcv", text, memberId: CUSTOMER_ID, ...(msgType ? {msgType} : {})}) + return { + type: "newChatItems" as const, + user: makeUser(GROK_USER_ID), + chatItems: [{chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID)}, chatItem: ci}], + } +} + +// ═══════════════════════════════════════════════════════════ +// Tests +// ═══════════════════════════════════════════════════════════ + +describe("Welcome & First Message", () => { + beforeEach(() => setup()) + + test("first message → queue reply sent, card created in team group", async () => { + await bot.onNewChatItems(customerMessage("Hello")) + expectSentToGroup(CUSTOMER_GROUP_ID, "The team will reply to your message") + const teamMsgs = chat.sentTo(TEAM_GROUP_ID) + expect(teamMsgs.length).toBeGreaterThan(0) + expect(teamMsgs[teamMsgs.length - 1]).toContain(`/'join ${CUSTOMER_GROUP_ID}'`) + }) + + test("non-text first message → no queue reply, no card", async () => { + await bot.onNewChatItems(customerNonTextMessage()) + expectNotSentToGroup(CUSTOMER_GROUP_ID, "The team will reply to your message") + expect(chat.sentTo(TEAM_GROUP_ID).length).toBe(0) + }) + + test("second message → no duplicate queue reply", async () => { + await bot.onNewChatItems(customerMessage("Hello")) + addBotMessage("The team will reply to your message") + const countBefore = chat.sentTo(CUSTOMER_GROUP_ID).filter(m => m.includes("The team will reply to your message")).length + await bot.onNewChatItems(customerMessage("Second message")) + const countAfter = chat.sentTo(CUSTOMER_GROUP_ID).filter(m => m.includes("The team will reply to your message")).length + expect(countAfter).toBe(countBefore) + }) + + test("unrecognized /command → treated as normal message", async () => { + await bot.onNewChatItems(customerMessage("/unknown")) + expectSentToGroup(CUSTOMER_GROUP_ID, "The team will reply to your message") + }) +}) + +describe("/grok Activation", () => { + beforeEach(() => setup()) + + test("/grok from QUEUE → Grok invited, grokActivatedMessage sent", async () => { + await reachQueue() + addBotMessage("The team will reply to your message") + const joinPromise = simulateGrokJoinSuccess() + await bot.onNewChatItems(customerMessage("/grok")) + await joinPromise + await bot.flush() + expectMemberAdded(CUSTOMER_GROUP_ID, GROK_CONTACT_ID) + expectSentToGroup(CUSTOMER_GROUP_ID, grokActivatedMessage) + }) + + test("/grok as first message → WELCOME→GROK directly, no queue message", async () => { + const joinPromise = simulateGrokJoinSuccess() + await bot.onNewChatItems(customerMessage("/grok")) + await joinPromise + await bot.flush() + expectSentToGroup(CUSTOMER_GROUP_ID, grokActivatedMessage) + expectNotSentToGroup(CUSTOMER_GROUP_ID, "The team will reply to your message") + expect(chat.sentTo(TEAM_GROUP_ID).length).toBeGreaterThan(0) + }) + + test("/grok in TEAM → rejected with teamLockedMessage", async () => { + await reachTeam() + await bot.onNewChatItems(customerMessage("/grok")) + expectSentToGroup(CUSTOMER_GROUP_ID, teamLockedMessage) + }) + + test("/grok when grokContactId is null → grokUnavailableMessage", async () => { + setup({grokContactId: null}) + await reachQueue() + addBotMessage("The team will reply to your message") + await bot.onNewChatItems(customerMessage("/grok")) + await bot.flush() + expectSentToGroup(CUSTOMER_GROUP_ID, "temporarily unavailable") + }) + + test("/grok as first message + Grok join fails → queue message sent as fallback", async () => { + chat.apiAddMemberWillFail() + await bot.onNewChatItems(customerMessage("/grok")) + await bot.flush() + expectSentToGroup(CUSTOMER_GROUP_ID, "temporarily unavailable") + expectSentToGroup(CUSTOMER_GROUP_ID, "The team will reply to your message") + }) +}) + +describe("Grok Conversation", () => { + beforeEach(() => setup()) + + test("Grok per-message: reads history, calls API, sends response", async () => { + addCustomerMessageToHistory("How do I create a group?", GROK_LOCAL_GROUP_ID) + grokApi.willRespond("To create a group, tap +, then New Group.") + await bot.onGrokNewChatItems(grokViewCustomerMessage("How do I create a group?")) + + expect(grokApi.calls.length).toBe(1) + expect(grokApi.calls[0].message).toBe("How do I create a group?") + expectAnySent("To create a group, tap +, then New Group.") + }) + + test("customer non-text in GROK → no Grok API call", async () => { + await bot.onGrokNewChatItems(grokViewCustomerMessage("", "image")) + expect(grokApi.calls.length).toBe(0) + }) + + test("Grok API error → error message in group, stays GROK", async () => { + grokApi.willFail() + await bot.onGrokNewChatItems(grokViewCustomerMessage("A question")) + expectAnySent("couldn't process that") + }) + + test("Grok ignores bot commands from customer", async () => { + await bot.onGrokNewChatItems(grokViewCustomerMessage("/team")) + expect(grokApi.calls.length).toBe(0) + }) + + test("Grok per-message: history includes prior Grok sent response as assistant", async () => { + addCustomerMessageToHistory("How do I create a group?", GROK_LOCAL_GROUP_ID) + addBotMessage("To create a group, tap + then New Group.", GROK_LOCAL_GROUP_ID) + addCustomerMessageToHistory("How do I invite members?", GROK_LOCAL_GROUP_ID) + grokApi.willRespond("Open the group and tap Invite.") + await bot.onGrokNewChatItems(grokViewCustomerMessage("How do I invite members?")) + + expect(grokApi.calls.length).toBe(1) + expect(grokApi.calls[0].message).toBe("How do I invite members?") + expect(grokApi.calls[0].history).toEqual([ + {role: "user", content: "How do I create a group?"}, + {role: "assistant", content: "To create a group, tap + then New Group."}, + ]) + }) + + test("Grok ignores non-customer messages", async () => { + chat.groups.set(GROK_LOCAL_GROUP_ID, makeGroupInfo(GROK_LOCAL_GROUP_ID)) + const ci = makeChatItem({dir: "groupRcv", text: "Team message", memberId: "not-customer", memberContactId: TEAM_MEMBER_1_ID}) + const grokEvt = { + type: "newChatItems" as const, + user: makeUser(GROK_USER_ID), + chatItems: [{chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID)}, chatItem: ci}], + } + await bot.onGrokNewChatItems(grokEvt) + expect(grokApi.calls.length).toBe(0) + }) + + test("Grok ignores own messages (groupSnd)", async () => { + chat.groups.set(GROK_LOCAL_GROUP_ID, makeGroupInfo(GROK_LOCAL_GROUP_ID)) + const ci = makeChatItem({dir: "groupSnd", text: "My own response"}) + const grokEvt = { + type: "newChatItems" as const, + user: makeUser(GROK_USER_ID), + chatItems: [{chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID)}, chatItem: ci}], + } + await bot.onGrokNewChatItems(grokEvt) + expect(grokApi.calls.length).toBe(0) + }) + + test("batch: multiple customer messages in one event → only last triggers Grok API call", async () => { + chat.groups.set(GROK_LOCAL_GROUP_ID, makeGroupInfo(GROK_LOCAL_GROUP_ID)) + addCustomerMessageToHistory("First question", GROK_LOCAL_GROUP_ID) + addCustomerMessageToHistory("Second question", GROK_LOCAL_GROUP_ID) + + const ci1 = makeChatItem({dir: "groupRcv", text: "First question", memberId: CUSTOMER_ID}) + const ci2 = makeChatItem({dir: "groupRcv", text: "Second question", memberId: CUSTOMER_ID}) + const evt = { + type: "newChatItems" as const, + user: makeUser(GROK_USER_ID), + chatItems: [ + {chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID)}, chatItem: ci1}, + {chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID)}, chatItem: ci2}, + ], + } + + await bot.onGrokNewChatItems(evt) + + expect(grokApi.calls.length).toBe(1) + expect(grokApi.calls[0].message).toBe("Second question") + }) + + test("batch: messages from different groups → each group gets one response", async () => { + const GROK_GROUP_A = 201 + const GROK_GROUP_B = 202 + chat.groups.set(GROK_GROUP_A, makeGroupInfo(GROK_GROUP_A)) + chat.groups.set(GROK_GROUP_B, makeGroupInfo(GROK_GROUP_B)) + addCustomerMessageToHistory("Question A", GROK_GROUP_A) + addCustomerMessageToHistory("Question B", GROK_GROUP_B) + + const ciA = makeChatItem({dir: "groupRcv", text: "Question A", memberId: CUSTOMER_ID}) + const ciB = makeChatItem({dir: "groupRcv", text: "Question B", memberId: CUSTOMER_ID}) + const evt = { + type: "newChatItems" as const, + user: makeUser(GROK_USER_ID), + chatItems: [ + {chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_GROUP_A)}, chatItem: ciA}, + {chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_GROUP_B)}, chatItem: ciB}, + ], + } + + await bot.onGrokNewChatItems(evt) + + expect(grokApi.calls.length).toBe(2) + }) + + test("batch: non-customer messages filtered, only customer messages trigger response", async () => { + chat.groups.set(GROK_LOCAL_GROUP_ID, makeGroupInfo(GROK_LOCAL_GROUP_ID)) + addCustomerMessageToHistory("Customer question", GROK_LOCAL_GROUP_ID) + + const custCi = makeChatItem({dir: "groupRcv", text: "Customer question", memberId: CUSTOMER_ID}) + const teamCi = makeChatItem({dir: "groupRcv", text: "Team reply", memberId: "not-customer", memberContactId: TEAM_MEMBER_1_ID}) + const evt = { + type: "newChatItems" as const, + user: makeUser(GROK_USER_ID), + chatItems: [ + {chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID)}, chatItem: custCi}, + {chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID)}, chatItem: teamCi}, + ], + } + + await bot.onGrokNewChatItems(evt) + + expect(grokApi.calls.length).toBe(1) + expect(grokApi.calls[0].message).toBe("Customer question") + }) + + test("batch: across groups → Grok calls overlap in-flight (parallel dispatch)", async () => { + const GROK_GROUP_A = 201 + const GROK_GROUP_B = 202 + chat.groups.set(GROK_GROUP_A, makeGroupInfo(GROK_GROUP_A)) + chat.groups.set(GROK_GROUP_B, makeGroupInfo(GROK_GROUP_B)) + addCustomerMessageToHistory("A", GROK_GROUP_A) + addCustomerMessageToHistory("B", GROK_GROUP_B) + + const ciA = makeChatItem({dir: "groupRcv", text: "A", memberId: CUSTOMER_ID}) + const ciB = makeChatItem({dir: "groupRcv", text: "B", memberId: CUSTOMER_ID}) + const evt = { + type: "newChatItems" as const, + user: makeUser(GROK_USER_ID), + chatItems: [ + {chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_GROUP_A)}, chatItem: ciA}, + {chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_GROUP_B)}, chatItem: ciB}, + ], + } + + // Block both chat() calls until we release. If the handler serialized + // per-group work, only one call would enter chat() before release. + grokApi.blockChat() + const done = bot.onGrokNewChatItems(evt) + // Let both tasks run up to the gate. + await new Promise(r => setTimeout(r, 10)) + expect(grokApi.calls.length).toBe(2) + grokApi.releaseChat() + await done + }) +}) + +describe("/team Activation", () => { + beforeEach(() => setup()) + + test("/team from QUEUE → ALL team members added, teamAddedMessage sent", async () => { + await reachQueue() + addBotMessage("The team will reply to your message") + await bot.onNewChatItems(customerMessage("/team")) + expectMemberAdded(CUSTOMER_GROUP_ID, TEAM_MEMBER_1_ID) + expectMemberAdded(CUSTOMER_GROUP_ID, TEAM_MEMBER_2_ID) + expectSentToGroup(CUSTOMER_GROUP_ID, "We will reply within") + }) + + test("/team as first message → WELCOME→TEAM, no queue message", async () => { + await bot.onNewChatItems(customerMessage("/team")) + expectSentToGroup(CUSTOMER_GROUP_ID, "We will reply within") + expectNotSentToGroup(CUSTOMER_GROUP_ID, "The team will reply to your message") + }) + + test("/team when already activated → teamAlreadyInvitedMessage", async () => { + await reachTeamPending() + addBotMessage("We will reply within 24 hours.") + chat.members.set(CUSTOMER_GROUP_ID, [makeTeamMember(TEAM_MEMBER_1_ID, "Alice")]) + await bot.onNewChatItems(customerMessage("/team")) + expectSentToGroup(CUSTOMER_GROUP_ID, teamAlreadyInvitedMessage) + }) + + test("/team with no team members → noTeamMembersMessage", async () => { + setup({teamMembers: []}) + await reachQueue() + addBotMessage("The team will reply to your message") + await bot.onNewChatItems(customerMessage("/team")) + expectSentToGroup(CUSTOMER_GROUP_ID, "No team members are available") + }) +}) + +describe("One-Way Gate", () => { + beforeEach(() => setup()) + + test("team member sends first TEXT → Grok removed if present", async () => { + await reachTeamPending() + addBotMessage("We will reply within 24 hours.") + chat.members.set(CUSTOMER_GROUP_ID, [makeGrokMember(), makeTeamMember(TEAM_MEMBER_1_ID, "Alice")]) + await bot.onNewChatItems(teamMemberMessage("Hi, how can I help?")) + expect(chat.removed.some(r => r.groupId === CUSTOMER_GROUP_ID && r.memberIds.includes(7777))).toBe(true) + }) + + test("team member non-text (no ciContentText) → Grok NOT removed", async () => { + await reachTeamPending() + addBotMessage("We will reply within 24 hours.") + chat.members.set(CUSTOMER_GROUP_ID, [makeGrokMember()]) + await bot.onNewChatItems(teamMemberMessage("", TEAM_MEMBER_1_ID)) + expect(chat.removed.length).toBe(0) + }) + + test("/grok after gate → teamLockedMessage", async () => { + await reachTeam() + await bot.onNewChatItems(customerMessage("/grok")) + expectSentToGroup(CUSTOMER_GROUP_ID, teamLockedMessage) + }) + + test("customer text in TEAM → card update scheduled, no bot reply", async () => { + await reachTeam() + const sentBefore = chat.sentTo(CUSTOMER_GROUP_ID).length + await bot.onNewChatItems(customerMessage("Follow-up question")) + const sentAfter = chat.sentTo(CUSTOMER_GROUP_ID).length + expect(sentAfter).toBe(sentBefore) + }) + + test("/grok in TEAM-PENDING → invite Grok if not present", async () => { + await reachTeamPending() + addBotMessage("We will reply within 24 hours.") + chat.members.set(CUSTOMER_GROUP_ID, [makeTeamMember(TEAM_MEMBER_1_ID, "Alice")]) + const joinPromise = simulateGrokJoinSuccess() + await bot.onNewChatItems(customerMessage("/grok")) + await joinPromise + await bot.flush() + expectMemberAdded(CUSTOMER_GROUP_ID, GROK_CONTACT_ID) + }) +}) + +describe("One-Way Gate with Grok Disabled", () => { + test("team text removes Grok even when grokApi is null", async () => { + setup() + // Recreate bot without grokApi but with grokContactId still set (simulates disabled Grok with persisted contact) + bot = new SupportBot(chat as any, null, config as any, MAIN_USER_ID, null, DESIRED_COMMANDS) + bot.cards = cards + // Reach QUEUE state with Grok + team member already present + addBotMessage("The team will reply to your message") + addBotMessage("We will reply within 24 hours.") + chat.members.set(CUSTOMER_GROUP_ID, [makeGrokMember(), makeTeamMember(TEAM_MEMBER_1_ID, "Alice")]) + // Team member sends text → one-way gate should fire + await bot.onNewChatItems(teamMemberMessage("Hi, how can I help?")) + expect(chat.removed.some(r => r.groupId === CUSTOMER_GROUP_ID && r.memberIds.includes(7777))).toBe(true) + }) + + test("Grok does not respond when disabled even if grokContactId is set", async () => { + setup() + bot = new SupportBot(chat as any, null, config as any, MAIN_USER_ID, null, DESIRED_COMMANDS) + bot.cards = cards + // Set up group with Grok member present + chat.members.set(CUSTOMER_GROUP_ID, [makeGrokMember()]) + addBotMessage("The team will reply to your message") + // Customer sends text in GROK state + await bot.onNewChatItems(customerMessage("How do I use SimpleX?")) + // Grok should not respond (grokApi is null) + expect(grokApi.calls.length).toBe(0) + }) +}) + +describe("Team Member Lifecycle", () => { + beforeEach(() => setup()) + + test("team member connected → promoted to Owner", async () => { + await bot.onMemberConnected(connectedEvent(CUSTOMER_GROUP_ID, makeTeamMember(TEAM_MEMBER_1_ID, "Alice"))) + expect(chat.roleChanges.some(r => r.groupId === CUSTOMER_GROUP_ID && r.memberIds.includes(5000 + TEAM_MEMBER_1_ID) && r.role === GroupMemberRole.Owner)).toBe(true) + }) + + test("/team invites team member → apiSetMembersRole(Owner) called at invite time", async () => { + await bot.onNewChatItems(customerMessage("/team")) + expectMemberAdded(CUSTOMER_GROUP_ID, TEAM_MEMBER_1_ID) + expect(chat.roleChanges.some(r => + r.groupId === CUSTOMER_GROUP_ID + && r.memberIds.includes(5000 + TEAM_MEMBER_1_ID) + && r.role === GroupMemberRole.Owner + )).toBe(true) + }) + + test("/join invites team member → apiSetMembersRole(Owner) called at invite time", async () => { + await bot.onNewChatItems(teamGroupMessage(`/join ${CUSTOMER_GROUP_ID}`)) + expectMemberAdded(CUSTOMER_GROUP_ID, TEAM_MEMBER_1_ID) + expect(chat.roleChanges.some(r => + r.groupId === CUSTOMER_GROUP_ID + && r.memberIds.includes(5000 + TEAM_MEMBER_1_ID) + && r.role === GroupMemberRole.Owner + )).toBe(true) + }) + + test("/team when team member already in group (any non-terminal status) → apiSetMembersRole NOT re-called", async () => { + chat.members.set(CUSTOMER_GROUP_ID, [makeTeamMember(TEAM_MEMBER_1_ID, "Alice")]) + await cards.mergeCustomData(CUSTOMER_GROUP_ID, {state: "TEAM-PENDING"}) + chat.added.length = 0 + chat.roleChanges.length = 0 + + await bot.onNewChatItems(customerMessage("/team")) + expect(chat.added.length).toBe(0) + expect(chat.roleChanges.length).toBe(0) + }) + + test("/join when team member already in group → apiSetMembersRole NOT re-called", async () => { + chat.members.set(CUSTOMER_GROUP_ID, [makeTeamMember(TEAM_MEMBER_1_ID, "Alice")]) + chat.added.length = 0 + chat.roleChanges.length = 0 + + await bot.onNewChatItems(teamGroupMessage(`/join ${CUSTOMER_GROUP_ID}`)) + expect(chat.added.length).toBe(0) + expect(chat.roleChanges.length).toBe(0) + }) + + test("customer connected → NOT promoted to Owner", async () => { + await bot.onMemberConnected(connectedEvent(CUSTOMER_GROUP_ID, makeCustomerMember())) + expect(chat.roleChanges.length).toBe(0) + }) + + test("Grok connected → NOT promoted to Owner", async () => { + await bot.onMemberConnected(connectedEvent(CUSTOMER_GROUP_ID, makeGrokMember())) + expect(chat.roleChanges.length).toBe(0) + }) + + test("all team members leave before sending → state stays TEAM-PENDING", async () => { + await reachTeamPending() + addBotMessage("We will reply within 24 hours.") + // Remove team members from the group + chat.members.set(CUSTOMER_GROUP_ID, []) + // State is authoritative and monotonic — composition changes never demote it. + // Customer is still waiting for the team's response. + const state = await cards.deriveState(CUSTOMER_GROUP_ID) + expect(state).toBe("TEAM-PENDING") + }) + + test("/team after all team members left (TEAM-PENDING, no msg sent) → re-adds members", async () => { + await reachTeamPending() + addBotMessage("We will reply within 24 hours.") + chat.members.set(CUSTOMER_GROUP_ID, []) + chat.added.length = 0 + + await bot.onNewChatItems(customerMessage("/team")) + expectSentToGroup(CUSTOMER_GROUP_ID, "We will reply within") + expectMemberAdded(CUSTOMER_GROUP_ID, TEAM_MEMBER_1_ID) + }) + + test("/team after all team members left (TEAM, msg was sent) → re-adds members", async () => { + await reachTeamPending() + addBotMessage("We will reply within 24 hours.") + chat.members.set(CUSTOMER_GROUP_ID, [makeTeamMember(TEAM_MEMBER_1_ID, "Alice")]) + addTeamMemberMessageToHistory("Hi, how can I help?", TEAM_MEMBER_1_ID) + await bot.onNewChatItems(teamMemberMessage("Hi, how can I help?")) + + // All team members leave + chat.members.set(CUSTOMER_GROUP_ID, []) + chat.added.length = 0 + + await bot.onNewChatItems(customerMessage("/team")) + expectSentToGroup(CUSTOMER_GROUP_ID, "We will reply within") + expectMemberAdded(CUSTOMER_GROUP_ID, TEAM_MEMBER_1_ID) + }) +}) + +describe("Card Dashboard", () => { + beforeEach(() => setup()) + + test("first message creates card with /'join ' final line", async () => { + await bot.onNewChatItems(customerMessage("Hello")) + const teamMsgs = chat.sentTo(TEAM_GROUP_ID) + expect(teamMsgs.length).toBe(1) + const card = teamMsgs[0] + expect(card).toContain(`/'join ${CUSTOMER_GROUP_ID}'`) + // Join command is the final line of the card + const lines = card.split("\n") + expect(lines[lines.length - 1]).toBe(`/'join ${CUSTOMER_GROUP_ID}'`) + }) + + test("card update deletes old card then posts new one", async () => { + chat.customData.set(CUSTOMER_GROUP_ID, {cardItemId: 555}) + await cards.flush() + expect(chat.deleted.length).toBe(0) + + cards.scheduleUpdate(CUSTOMER_GROUP_ID) + await cards.flush() + expectCardDeleted(555) + expect(chat.sentTo(TEAM_GROUP_ID).length).toBeGreaterThan(0) + }) + + test("apiDeleteChatItems failure → ignored, new card posted", async () => { + chat.customData.set(CUSTOMER_GROUP_ID, {cardItemId: 555}) + chat.apiDeleteChatItemsWillFail() + cards.scheduleUpdate(CUSTOMER_GROUP_ID) + await cards.flush() + // New card should still be posted despite delete failure + expect(chat.sentTo(TEAM_GROUP_ID).length).toBeGreaterThan(0) + }) + + test("customData stores cardItemId → survives flush cycle", async () => { + await bot.onNewChatItems(customerMessage("Hello")) + // After card creation, customData should have cardItemId + const data = chat.customData.get(CUSTOMER_GROUP_ID) + expect(data).toBeDefined() + expect(typeof data.cardItemId).toBe("number") + }) + + test("concurrent mergeCustomData on same group → both patches survive", async () => { + // Without per-group serialization, two overlapping mergeCustomData calls + // can both read the same snapshot and the second write clobbers the first + // patch. The mutex keeps them ordered so both patches land. + const GID = 500 + chat.groups.set(GID, makeGroupInfo(GID)) + + const pA = cards.mergeCustomData(GID, {state: "QUEUE"}) + const pB = cards.mergeCustomData(GID, {cardItemId: 123}) + await Promise.all([pA, pB]) + + const final = chat.customData.get(GID) + expect(final.state).toBe("QUEUE") + expect(final.cardItemId).toBe(123) + }) + + test("customer leaves → customData cleared", async () => { + await bot.onNewChatItems(customerMessage("Hello")) + chat.customData.set(CUSTOMER_GROUP_ID, {cardItemId: 999}) + await bot.onLeftMember(leftEvent(CUSTOMER_GROUP_ID, makeCustomerMember())) + expect(chat.customData.has(CUSTOMER_GROUP_ID)).toBe(false) + }) +}) + +describe("Card Debouncing", () => { + beforeEach(() => setup()) + + test("rapid events within flush interval → single card update on flush", async () => { + chat.customData.set(CUSTOMER_GROUP_ID, {cardItemId: 500}) + cards.scheduleUpdate(CUSTOMER_GROUP_ID) + cards.scheduleUpdate(CUSTOMER_GROUP_ID) + cards.scheduleUpdate(CUSTOMER_GROUP_ID) + await cards.flush() + // Only one delete and one post + expect(chat.deleted.length).toBe(1) + // Multiple schedules → single update (one card message) + const teamMsgs = chat.sentTo(TEAM_GROUP_ID) + expect(teamMsgs.length).toBe(1) + }) + + test("multiple groups pending → each reposted once per flush", async () => { + const GROUP_A = 101 + const GROUP_B = 102 + chat.groups.set(GROUP_A, makeGroupInfo(GROUP_A)) + chat.groups.set(GROUP_B, makeGroupInfo(GROUP_B)) + chat.customData.set(GROUP_A, {cardItemId: 501}) + chat.customData.set(GROUP_B, {cardItemId: 502}) + cards.scheduleUpdate(GROUP_A) + cards.scheduleUpdate(GROUP_B) + await cards.flush() + expect(chat.deleted.length).toBe(2) + }) + + test("card create is immediate (not debounced)", async () => { + await bot.onNewChatItems(customerMessage("Hello")) + // Card should be posted immediately without flush + expect(chat.sentTo(TEAM_GROUP_ID).length).toBeGreaterThan(0) + }) + + test("flush with no pending updates → no-op", async () => { + await cards.flush() + expect(chat.deleted.length).toBe(0) + expect(chat.sentTo(TEAM_GROUP_ID).length).toBe(0) + }) + + test("flush on group with no cardItemId → createCard posts a new card (retries failed create)", async () => { + // customData without cardItemId simulates a prior createCard that failed + // mid-flight and re-queued itself. flushOne must dispatch to createCard, + // not updateCard (which would early-return). + const GID = 777 + chat.groups.set(GID, makeGroupInfo(GID)) + chat.customData.set(GID, {state: "QUEUE"}) + cards.scheduleUpdate(GID) + await cards.flush() + expect(chat.sentTo(TEAM_GROUP_ID).length).toBe(1) + expect(typeof chat.customData.get(GID).cardItemId).toBe("number") + }) +}) + +describe("Card Format & State Derivation", () => { + beforeEach(() => setup()) + + test("QUEUE state read from customData.state", async () => { + chat.customData.set(CUSTOMER_GROUP_ID, {cardItemId: 1234, state: "QUEUE"}) + const state = await cards.deriveState(CUSTOMER_GROUP_ID) + expect(state).toBe("QUEUE") + }) + + test("WELCOME state when customData.state is absent", async () => { + const state = await cards.deriveState(CUSTOMER_GROUP_ID) + expect(state).toBe("WELCOME") + }) + + test("GROK state read from customData.state", async () => { + chat.customData.set(CUSTOMER_GROUP_ID, {cardItemId: 1234, state: "GROK"}) + const state = await cards.deriveState(CUSTOMER_GROUP_ID) + expect(state).toBe("GROK") + }) + + test("TEAM-PENDING state read from customData.state", async () => { + chat.customData.set(CUSTOMER_GROUP_ID, {cardItemId: 1234, state: "TEAM-PENDING"}) + const state = await cards.deriveState(CUSTOMER_GROUP_ID) + expect(state).toBe("TEAM-PENDING") + }) + + test("TEAM state read from customData.state", async () => { + chat.customData.set(CUSTOMER_GROUP_ID, {cardItemId: 1234, state: "TEAM"}) + const state = await cards.deriveState(CUSTOMER_GROUP_ID) + expect(state).toBe("TEAM") + }) + + test("message count excludes bot's own messages", async () => { + addCustomerMessageToHistory("Hello") + addBotMessage("Queue message") + addCustomerMessageToHistory("Follow-up") + const chatResult = await cards.getChat(CUSTOMER_GROUP_ID, 100) + const nonBotCount = chatResult.chatItems.filter((ci: any) => ci.chatDir.type !== "groupSnd").length + expect(nonBotCount).toBe(2) + }) +}) + +describe("/join Command (Team Group)", () => { + beforeEach(() => setup()) + + test("/join (numeric only) → team member added to customer group", async () => { + await bot.onNewChatItems(teamGroupMessage(`/join ${CUSTOMER_GROUP_ID}`)) + expectMemberAdded(CUSTOMER_GROUP_ID, TEAM_MEMBER_1_ID) + }) + + test("/join : (legacy form) → team member added", async () => { + await bot.onNewChatItems(teamGroupMessage(`/join ${CUSTOMER_GROUP_ID}:Customer`)) + expectMemberAdded(CUSTOMER_GROUP_ID, TEAM_MEMBER_1_ID) + }) + + test("/join validates target is business group → error if not", async () => { + const nonBizGroupId = 999 + chat.groups.set(nonBizGroupId, makeGroupInfo(nonBizGroupId, {businessChat: null})) + await bot.onNewChatItems(teamGroupMessage(`/join ${nonBizGroupId}:Test`)) + expectSentToGroup(TEAM_GROUP_ID, "not a business chat") + }) + + test("/join with non-existent groupId → error in team group", async () => { + await bot.onNewChatItems(teamGroupMessage("/join 99999:Nobody")) + expect(chat.sentTo(TEAM_GROUP_ID).some(m => m.toLowerCase().includes("error"))).toBe(true) + }) + + test("customer sending /join in customer group → treated as normal message", async () => { + await bot.onNewChatItems(customerMessage("/join 50:Test")) + expectSentToGroup(CUSTOMER_GROUP_ID, "The team will reply to your message") + }) + + test("/join with non-numeric params → error reply, no apiAddMember call", async () => { + await bot.onNewChatItems(teamGroupMessage("/join abc")) + expectSentToGroup(TEAM_GROUP_ID, `Error: invalid group id "abc"`) + expect(chat.added.length).toBe(0) + }) +}) + +describe("DM Handshake", () => { + beforeEach(() => setup()) + + test("team member joins team group → DM sent with contact ID", async () => { + const member = {memberId: "new-team", groupMemberId: 8000, memberContactId: 30, memberStatus: GroupMemberStatus.Connected, memberProfile: {displayName: "Charlie"}} + await bot.onMemberConnected(connectedEvent(TEAM_GROUP_ID, member, {contactId: 30, profile: {displayName: "Charlie"}})) + expectDmSent(30, "Your contact ID is 30:Charlie") + }) + + test("DM with spaces in name → name single-quoted", async () => { + const member = {memberId: "new-team", groupMemberId: 8001, memberContactId: 31, memberStatus: GroupMemberStatus.Connected, memberProfile: {displayName: "Charlie Brown"}} + await bot.onMemberConnected(connectedEvent(TEAM_GROUP_ID, member, {contactId: 31, profile: {displayName: "Charlie Brown"}})) + expectDmSent(31, "31:'Charlie Brown'") + }) + + test("pending DM delivered on contactConnected", async () => { + const invEvt = { + type: "newMemberContactReceivedInv" as const, + user: makeUser(MAIN_USER_ID), + contact: {contactId: 32, profile: {displayName: "Dave"}}, + groupInfo: makeGroupInfo(TEAM_GROUP_ID, {businessChat: null}), + member: {memberId: "dave-member", groupMemberId: 8002, memberContactId: 32, memberStatus: GroupMemberStatus.Connected, memberProfile: {displayName: "Dave"}}, + } + await bot.onMemberContactReceivedInv(invEvt) + + await bot.onContactConnected({ + type: "contactConnected" as const, + user: makeUser(MAIN_USER_ID), + contact: {contactId: 32, profile: {displayName: "Dave"}}, + }) + expectDmSent(32, "Your contact ID is 32:Dave") + }) + + test("team member with no DM contact → creates member contact and sends invitation", async () => { + const member = {memberId: "new-team-no-dm", groupMemberId: 8010, memberContactId: null, memberStatus: GroupMemberStatus.Connected, memberProfile: {displayName: "Frank"}} + await bot.onMemberConnected(connectedEvent(TEAM_GROUP_ID, member, undefined)) + expect(chat.memberContacts.some(c => c.groupId === TEAM_GROUP_ID && c.groupMemberId === 8010)).toBe(true) + expect(chat.memberContactInvitations.some(i => i.text.includes("Your contact ID is") && i.text.includes("Frank"))).toBe(true) + const dms = chat.sent.filter(s => s.chat[0] === ChatType.Direct) + expect(dms.some(m => m.text.includes("Your contact ID is") && m.text.includes("Frank"))).toBe(true) + }) + + test("joinedGroupMember in team group → creates member contact and sends invitation", async () => { + const member = {memberId: "link-joiner", groupMemberId: 8020, memberContactId: null, memberStatus: GroupMemberStatus.Connected, memberProfile: {displayName: "Grace"}} + await bot.onJoinedGroupMember(joinedEvent(TEAM_GROUP_ID, member)) + expect(chat.memberContacts.some(c => c.groupId === TEAM_GROUP_ID && c.groupMemberId === 8020)).toBe(true) + expect(chat.memberContactInvitations.some(i => i.text.includes("Grace"))).toBe(true) + }) + + test("no duplicate DM when both sendTeamMemberDM succeeds and onMemberContactReceivedInv fires", async () => { + const invEvt = { + type: "newMemberContactReceivedInv" as const, + user: makeUser(MAIN_USER_ID), + contact: {contactId: 33, profile: {displayName: "Eve"}}, + groupInfo: makeGroupInfo(TEAM_GROUP_ID, {businessChat: null}), + member: {memberId: "eve-member", groupMemberId: 8003, memberContactId: 33, memberStatus: GroupMemberStatus.Connected, memberProfile: {displayName: "Eve"}}, + } + await bot.onMemberContactReceivedInv(invEvt) + + const eveMember = {memberId: "eve-member", groupMemberId: 8003, memberContactId: 33, memberStatus: GroupMemberStatus.Connected, memberProfile: {displayName: "Eve"}} + await bot.onMemberConnected(connectedEvent(TEAM_GROUP_ID, eveMember, {contactId: 33, profile: {displayName: "Eve"}})) + + await bot.onContactConnected({ + type: "contactConnected" as const, + user: makeUser(MAIN_USER_ID), + contact: {contactId: 33, profile: {displayName: "Eve"}}, + }) + + const dms = chat.sentDirect(33) + const contactIdMsgs = dms.filter(m => m.includes("Your contact ID is 33:Eve")) + expect(contactIdMsgs.length).toBe(1) + }) +}) + +describe("Direct Message Handling", () => { + beforeEach(() => setup()) + + test("regular DM → bot replies with business address link", async () => { + bot.businessAddress = "simplex:/contact#abc123" + await bot.onNewChatItems(directMessage("Hi there", 99)) + expectDmSent(99, "simplex:/contact#abc123") + }) + + test("DM without business address set → no reply", async () => { + bot.businessAddress = null + await bot.onNewChatItems(directMessage("Hi there", 99)) + expect(chat.sentDirect(99).length).toBe(0) + }) + + test("non-message DM event (e.g. contactConnected) → no reply", async () => { + bot.businessAddress = "simplex:/contact#abc123" + const ci = { + chatDir: {type: "directRcv"}, + content: {type: "rcvDirectEvent"}, + meta: {itemId: 9999, createdAt: new Date().toISOString()}, + } + const evt = { + type: "newChatItems" as const, + user: makeUser(MAIN_USER_ID), + chatItems: [makeDirectAChatItem(ci, 99)], + } + await bot.onNewChatItems(evt) + expect(chat.sentDirect(99).length).toBe(0) + }) +}) + +describe("Business Request Handler", () => { + beforeEach(() => setup()) + + test("acceptingBusinessRequest → enables file uploads AND visible history", async () => { + await bot.onBusinessRequest({ + type: "acceptingBusinessRequest" as const, + user: makeUser(MAIN_USER_ID), + groupInfo: makeGroupInfo(CUSTOMER_GROUP_ID), + }) + expect(chat.profileUpdates.some(u => + u.groupId === CUSTOMER_GROUP_ID + && u.profile.groupPreferences?.files?.enable === GroupFeatureEnabled.On + && u.profile.groupPreferences?.history?.enable === GroupFeatureEnabled.On + )).toBe(true) + }) +}) + +describe("chatItemUpdated Handler", () => { + beforeEach(() => setup()) + + test("chatItemUpdated in business group → card update scheduled", async () => { + await bot.onChatItemUpdated(updatedEvent(CUSTOMER_GROUP_ID, makeChatItem({dir: "groupRcv", text: "edited message", memberId: CUSTOMER_ID}))) + chat.customData.set(CUSTOMER_GROUP_ID, {cardItemId: 600}) + await cards.flush() + expectCardDeleted(600) + }) + + test("chatItemUpdated in non-business group → ignored", async () => { + await bot.onChatItemUpdated(updatedEvent(TEAM_GROUP_ID, makeChatItem({dir: "groupRcv", text: "team msg"}))) + await cards.flush() + expect(chat.deleted.length).toBe(0) + }) + + test("chatItemUpdated from wrong user → ignored", async () => { + await bot.onChatItemUpdated(updatedEvent(CUSTOMER_GROUP_ID, makeChatItem({dir: "groupRcv", text: "edited"}), GROK_USER_ID)) + await cards.flush() + expect(chat.deleted.length).toBe(0) + }) +}) + +describe("Reactions", () => { + beforeEach(() => setup()) + + test("reaction in business group → card update scheduled", async () => { + await bot.onChatItemReaction(reactionEvent(CUSTOMER_GROUP_ID, true)) + chat.customData.set(CUSTOMER_GROUP_ID, {cardItemId: 700}) + await cards.flush() + expectCardDeleted(700) + }) + + test("reaction removed (added=false) → no card update", async () => { + await bot.onChatItemReaction(reactionEvent(CUSTOMER_GROUP_ID, false)) + await cards.flush() + expect(chat.deleted.length).toBe(0) + }) +}) + +describe("Customer Leave", () => { + beforeEach(() => setup()) + + test("customer leaves → customData cleared", async () => { + chat.customData.set(CUSTOMER_GROUP_ID, {cardItemId: 800}) + await bot.onLeftMember(leftEvent(CUSTOMER_GROUP_ID, makeCustomerMember())) + expect(chat.customData.has(CUSTOMER_GROUP_ID)).toBe(false) + }) + + test("Grok leaves → in-memory maps cleaned", async () => { + await bot.onLeftMember(leftEvent(CUSTOMER_GROUP_ID, makeGrokMember())) + }) + + test("team member leaves → logged, no crash", async () => { + await bot.onLeftMember(leftEvent(CUSTOMER_GROUP_ID, makeTeamMember(TEAM_MEMBER_1_ID, "Alice"))) + }) + + test("leftMember in non-business group → ignored", async () => { + const member = {memberId: "someone", groupMemberId: 9000, memberStatus: GroupMemberStatus.Connected, memberProfile: {displayName: "Someone"}} + await bot.onLeftMember(leftEvent(TEAM_GROUP_ID, member)) + }) +}) + +describe("Error Handling", () => { + beforeEach(() => setup()) + + test("apiAddMember fails (Grok invite) → grokUnavailableMessage", async () => { + await reachQueue() + addBotMessage("The team will reply to your message") + chat.apiAddMemberWillFail() + await bot.onNewChatItems(customerMessage("/grok")) + await bot.flush() + expectSentToGroup(CUSTOMER_GROUP_ID, "temporarily unavailable") + }) + + test("groupDuplicateMember on Grok invite → only inviting message, no result", async () => { + await reachQueue() + addBotMessage("The team will reply to your message") + chat.apiAddMemberWillFail({chatError: {errorType: {type: "groupDuplicateMember"}}}) + const sentBefore = chat.sent.length + await bot.onNewChatItems(customerMessage("/grok")) + await bot.flush() + // Only the "Inviting Grok" message is sent — no activated/unavailable result + expect(chat.sent.length).toBe(sentBefore + 1) + expectSentToGroup(CUSTOMER_GROUP_ID, "Inviting Grok") + expectNotSentToGroup(CUSTOMER_GROUP_ID, grokActivatedMessage) + expectNotSentToGroup(CUSTOMER_GROUP_ID, "temporarily unavailable") + }) + + test("/team while members are in Invited status → no second apiAddMember call", async () => { + await reachTeamPending() + addBotMessage("We will reply within 24 hours.") + + // Simulate the realistic post-/team state: both members have been invited + // but have not yet accepted (memberStatus = "invited"). The SimpleX API + // would resend the invitation if apiAddMember is called for an Invited + // member — the pre-check in addOrFindTeamMember must skip them. + const invited = (contactId: number) => ({ + memberId: `team-${contactId}`, + groupMemberId: 5000 + contactId, + memberContactId: contactId, + memberStatus: "invited", + memberProfile: {displayName: `Contact${contactId}`}, + }) + chat.members.set(CUSTOMER_GROUP_ID, [invited(TEAM_MEMBER_1_ID), invited(TEAM_MEMBER_2_ID)]) + chat.added.length = 0 + + await bot.onNewChatItems(customerMessage("/team")) + expect(chat.added.filter(a => a.groupId === CUSTOMER_GROUP_ID)).toEqual([]) + }) + + test("/grok in TEAM-PENDING while Grok is in Invited status → no second apiAddMember call", async () => { + await reachTeamPending() + addBotMessage("We will reply within 24 hours.") + chat.members.set(CUSTOMER_GROUP_ID, [ + makeTeamMember(TEAM_MEMBER_1_ID, "Alice"), + {memberId: "grok-member", groupMemberId: 7777, memberContactId: GROK_CONTACT_ID, memberStatus: "invited", memberProfile: {displayName: "Grok"}}, + ]) + chat.added.length = 0 + + await bot.onNewChatItems(customerMessage("/grok")) + await bot.flush() + + expect(chat.added.filter(a => a.contactId === GROK_CONTACT_ID)).toEqual([]) + expectNotSentToGroup(CUSTOMER_GROUP_ID, "Inviting Grok") + }) +}) + +describe("Profile / Event Filtering", () => { + beforeEach(() => setup()) + + test("newChatItems from Grok profile → ignored by main handler", async () => { + const evt = { + type: "newChatItems" as const, + user: makeUser(GROK_USER_ID), + chatItems: [makeAChatItem(makeChatItem({dir: "groupRcv", text: "test"}))], + } + const sentBefore = chat.sent.length + await bot.onNewChatItems(evt) + expect(chat.sent.length).toBe(sentBefore) + }) + + test("Grok events from main profile → ignored by Grok handlers", async () => { + const evt = { + type: "receivedGroupInvitation" as const, + user: makeUser(MAIN_USER_ID), + groupInfo: makeGroupInfo(300), + contact: {contactId: 1}, + fromMemberRole: GroupMemberRole.Admin, + memberRole: GroupMemberRole.Member, + } + await bot.onGrokGroupInvitation(evt) + expect(chat.joined.length).toBe(0) + }) + + test("own messages (groupSnd) → ignored", async () => { + const ci = makeChatItem({dir: "groupSnd", text: "Bot message"}) + const evt = { + type: "newChatItems" as const, + user: makeUser(MAIN_USER_ID), + chatItems: [makeAChatItem(ci)], + } + const sentBefore = chat.sent.length + await bot.onNewChatItems(evt) + expect(chat.sent.length).toBe(sentBefore) + }) + + test("non-business group messages → ignored", async () => { + const ci = makeChatItem({dir: "groupRcv", text: "test"}) + const nonBizGroup = makeGroupInfo(999, {businessChat: null}) + const evt = { + type: "newChatItems" as const, + user: makeUser(MAIN_USER_ID), + chatItems: [{chatInfo: {type: "group", groupInfo: nonBizGroup}, chatItem: ci}], + } + const sentBefore = chat.sent.length + await bot.onNewChatItems(evt) + expect(chat.sent.length).toBe(sentBefore) + }) +}) + +describe("Grok Join Flow", () => { + beforeEach(() => setup()) + + test("Grok receivedGroupInvitation → apiJoinGroup called", async () => { + // First need to set up a pending grok join + // Simulate the main profile side: add Grok to a group + await reachQueue() + addBotMessage("The team will reply to your message") + + // This kicks off activateGrok which adds member and waits + const joinComplete = new Promise(async (resolve) => { + // Simulate Grok invitation after a small delay + setTimeout(async () => { + const addedGrok = chat.added.find(a => a.contactId === GROK_CONTACT_ID) + if (addedGrok) { + const memberId = `member-${GROK_CONTACT_ID}` + await bot.onGrokGroupInvitation({ + type: "receivedGroupInvitation", + user: makeUser(GROK_USER_ID), + groupInfo: {...makeGroupInfo(GROK_LOCAL_GROUP_ID), membership: {memberId}}, + contact: {contactId: 99}, + fromMemberRole: GroupMemberRole.Admin, + memberRole: GroupMemberRole.Member, + }) + } + resolve() + }, 10) + }) + + // Don't await bot.onNewChatItems yet — let it start + const botPromise = bot.onNewChatItems(customerMessage("/grok")) + await joinComplete + // Complete the join + await bot.onGrokMemberConnected({ + type: "connectedToGroupMember", + user: makeUser(GROK_USER_ID), + groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID), + member: {memberId: "bot-in-grok-view", groupMemberId: 9999}, + }) + await botPromise + + expect(chat.joined).toContain(GROK_LOCAL_GROUP_ID) + }) + + test("unmatched Grok invitation → buffered, not joined", async () => { + const evt = { + type: "receivedGroupInvitation" as const, + user: makeUser(GROK_USER_ID), + groupInfo: {...makeGroupInfo(999), membership: {memberId: "unknown-member"}}, + contact: {contactId: 99}, + fromMemberRole: GroupMemberRole.Admin, + memberRole: GroupMemberRole.Member, + } + await bot.onGrokGroupInvitation(evt) + expect(chat.joined.length).toBe(0) + }) + + test("buffered invitation drained after pendingGrokJoins set → apiJoinGroup called", async () => { + // Simulate the race: invitation arrives before pendingGrokJoins is set + const memberId = `member-${GROK_CONTACT_ID}` + const invEvt = { + type: "receivedGroupInvitation" as const, + user: makeUser(GROK_USER_ID), + groupInfo: {...makeGroupInfo(GROK_LOCAL_GROUP_ID), membership: {memberId}}, + contact: {contactId: 99}, + fromMemberRole: GroupMemberRole.Admin, + memberRole: GroupMemberRole.Member, + } + // Buffer the invitation (no pending join registered yet) + await bot.onGrokGroupInvitation(invEvt) + expect(chat.joined.length).toBe(0) + + // Now trigger activateGrok — apiAddMember returns, pendingGrokJoins set, buffer drained + const joinComplete = new Promise((resolve) => { + setTimeout(async () => { + // Grok connected after buffer drain processed the invitation + await bot.onGrokMemberConnected({ + type: "connectedToGroupMember", + user: makeUser(GROK_USER_ID), + groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID), + member: {memberId: "bot-in-grok-view", groupMemberId: 9999}, + }) + resolve() + }, 20) + }) + + await reachQueue() + addBotMessage("The team will reply to your message") + const botPromise = bot.onNewChatItems(customerMessage("/grok")) + await joinComplete + await botPromise + await bot.flush() + + expect(chat.joined).toContain(GROK_LOCAL_GROUP_ID) + expectSentToGroup(CUSTOMER_GROUP_ID, grokActivatedMessage) + }) + + test("per-message responses suppressed during activateGrok initial response", async () => { + await reachQueue() + addBotMessage("The team will reply to your message") + + // Customer's message visible in Grok's view (activateGrok reads it for initial response) + addCustomerMessageToHistory("Hello, I need help", GROK_LOCAL_GROUP_ID) + chat.groups.set(GROK_LOCAL_GROUP_ID, makeGroupInfo(GROK_LOCAL_GROUP_ID)) + + // Start /grok activation (fireAndForget) + const botPromise = bot.onNewChatItems(customerMessage("/grok")) + + // Wait for apiAddMember to complete + await new Promise(r => setTimeout(r, 10)) + + // Simulate Grok invitation → sets grokGroupMap/reverseGrokMap + const memberId = `member-${GROK_CONTACT_ID}` + await bot.onGrokGroupInvitation({ + type: "receivedGroupInvitation", + user: makeUser(GROK_USER_ID), + groupInfo: {...makeGroupInfo(GROK_LOCAL_GROUP_ID), membership: {memberId}}, + contact: {contactId: 99}, + fromMemberRole: GroupMemberRole.Admin, + memberRole: GroupMemberRole.Member, + }) + + // grokInitialResponsePending is set, reverseGrokMap is set. + // Simulate per-message event (as if message backlog arrived for Grok profile) + await bot.onGrokNewChatItems(grokViewCustomerMessage("Hello, I need help")) + + // Gating: per-message handler must NOT have called Grok API + expect(grokApi.calls.length).toBe(0) + + // Now complete the join → activateGrok sends initial combined response + await bot.onGrokMemberConnected({ + type: "connectedToGroupMember", + user: makeUser(GROK_USER_ID), + groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID), + member: {memberId: "bot-in-grok-view", groupMemberId: 9999, memberContactId: undefined}, + }) + + await botPromise + await bot.flush() + + // Only 1 Grok API call: the initial combined response from activateGrok + expect(grokApi.calls.length).toBe(1) + expect(grokApi.calls[0].message).toContain("Hello, I need help") + }) + + test("per-message responses resume after activateGrok completes", async () => { + await reachGrok() + await bot.flush() + const callsAfterActivation = grokApi.calls.length + + // Send a new customer message via Grok's view — should be processed normally + addCustomerMessageToHistory("Follow-up question", GROK_LOCAL_GROUP_ID) + await bot.onGrokNewChatItems(grokViewCustomerMessage("Follow-up question")) + + expect(grokApi.calls.length).toBe(callsAfterActivation + 1) + expect(grokApi.calls[grokApi.calls.length - 1].message).toBe("Follow-up question") + }) + + test("activateGrok groupDuplicateMember path → gate cleared by outer finally", async () => { + // After reachGrok(), gate is cleared and reverseGrokMap is populated. + await reachGrok() + await bot.flush() + const callsBaseline = grokApi.calls.length + + // Second /grok while Grok is already present → apiAddMember throws duplicate. + // The outer try/finally must clear the gate even though the handler returns + // silently from inside the try — otherwise per-message responses stay + // suppressed for this group forever. + chat.apiAddMemberWillFail({chatError: {errorType: {type: "groupDuplicateMember"}}}) + await bot.onNewChatItems(customerMessage("/grok")) + await bot.flush() + + // Gate must be clear: a subsequent per-message event triggers Grok. + addCustomerMessageToHistory("another question", GROK_LOCAL_GROUP_ID) + await bot.onGrokNewChatItems(grokViewCustomerMessage("another question")) + expect(grokApi.calls.length).toBe(callsBaseline + 1) + }) +}) + +describe("Grok No-History Fallback", () => { + beforeEach(() => setup()) + + test("Grok joins but sees no customer messages → sends grokNoHistoryMessage", async () => { + chat.chatItems.set(GROK_LOCAL_GROUP_ID, []) + chat.groups.set(GROK_LOCAL_GROUP_ID, makeGroupInfo(GROK_LOCAL_GROUP_ID)) + + const grokJoinPromise = simulateGrokJoinSuccess() + await bot.onNewChatItems(customerMessage("/grok")) + await grokJoinPromise + await bot.flush() + expectAnySent("couldn't see your earlier messages") + }) +}) + +describe("Non-customer messages trigger card update", () => { + beforeEach(() => setup()) + + test("Grok response in customer group → card update scheduled", async () => { + await bot.onNewChatItems(grokResponseMessage("Grok says hi")) + chat.customData.set(CUSTOMER_GROUP_ID, {cardItemId: 900}) + await cards.flush() + expectCardDeleted(900) + }) + + test("team member message → card update scheduled", async () => { + await bot.onNewChatItems(teamMemberMessage("Team says hi")) + chat.customData.set(CUSTOMER_GROUP_ID, {cardItemId: 901}) + await cards.flush() + expectCardDeleted(901) + }) +}) + +describe("End-to-End Flows", () => { + beforeEach(() => setup()) + + test("WELCOME → QUEUE → /team → TEAM-PENDING → team msg → TEAM", async () => { + await bot.onNewChatItems(customerMessage("Help me")) + expectSentToGroup(CUSTOMER_GROUP_ID, "The team will reply to your message") + addBotMessage("The team will reply to your message") + + await bot.onNewChatItems(customerMessage("/team")) + expectSentToGroup(CUSTOMER_GROUP_ID, "We will reply within") + expectMemberAdded(CUSTOMER_GROUP_ID, TEAM_MEMBER_1_ID) + addBotMessage("We will reply within 24 hours.") + + chat.members.set(CUSTOMER_GROUP_ID, [makeTeamMember(TEAM_MEMBER_1_ID, "Alice")]) + const pendingState = await cards.deriveState(CUSTOMER_GROUP_ID) + expect(pendingState).toBe("TEAM-PENDING") + + addTeamMemberMessageToHistory("I'll help you", TEAM_MEMBER_1_ID) + await bot.onNewChatItems(teamMemberMessage("I'll help you")) + + const teamState = await cards.deriveState(CUSTOMER_GROUP_ID) + expect(teamState).toBe("TEAM") + }) + + test("WELCOME → /grok first msg → GROK", async () => { + const joinPromise = simulateGrokJoinSuccess() + await bot.onNewChatItems(customerMessage("/grok")) + await joinPromise + await bot.flush() + + expectSentToGroup(CUSTOMER_GROUP_ID, grokActivatedMessage) + expectNotSentToGroup(CUSTOMER_GROUP_ID, "The team will reply to your message") + expect(chat.sentTo(TEAM_GROUP_ID).length).toBeGreaterThan(0) + }) + + test("multiple concurrent conversations are independent", async () => { + const GROUP_A = 101 + const GROUP_B = 102 + chat.groups.set(GROUP_A, makeGroupInfo(GROUP_A, {customerId: "cust-a"})) + chat.groups.set(GROUP_B, makeGroupInfo(GROUP_B, {customerId: "cust-b"})) + + const ciA = makeChatItem({dir: "groupRcv", text: "Help A", memberId: "cust-a"}) + await bot.onNewChatItems({ + type: "newChatItems", + user: makeUser(MAIN_USER_ID), + chatItems: [{chatInfo: {type: "group", groupInfo: makeGroupInfo(GROUP_A, {customerId: "cust-a"})}, chatItem: ciA}], + }) + + const ciB = makeChatItem({dir: "groupRcv", text: "Help B", memberId: "cust-b"}) + await bot.onNewChatItems({ + type: "newChatItems", + user: makeUser(MAIN_USER_ID), + chatItems: [{chatInfo: {type: "group", groupInfo: makeGroupInfo(GROUP_B, {customerId: "cust-b"})}, chatItem: ciB}], + }) + + expectSentToGroup(GROUP_A, "The team will reply to your message") + expectSentToGroup(GROUP_B, "The team will reply to your message") + }) +}) + +describe("Message Templates", () => { + test("welcomeMessage is a non-empty string", () => { + expect(typeof welcomeMessage).toBe("string") + expect(welcomeMessage.length).toBeGreaterThan(0) + }) + + test("grokActivatedMessage mentions chatting with Grok", () => { + expect(grokActivatedMessage).toContain("chatting with Grok") + }) + + test("teamLockedMessage tells customer the team will handle the conversation", () => { + expect(teamLockedMessage).toContain("team") + }) + + test("queueMessage mentions hours", () => { + const msg = queueMessage("UTC", true) + expect(msg).toContain("hours") + }) +}) + +describe("State persistence in customData", () => { + beforeEach(() => setup()) + + test("first customer text writes state=QUEUE to customData", async () => { + await bot.onNewChatItems(customerMessage("Hello")) + expect(chat.customData.get(CUSTOMER_GROUP_ID)?.state).toBe("QUEUE") + }) + + test("/team writes state=TEAM-PENDING immediately (before team accepts)", async () => { + await reachQueue() + addBotMessage("The team will reply to your message") + await bot.onNewChatItems(customerMessage("/team")) + expect(chat.customData.get(CUSTOMER_GROUP_ID)?.state).toBe("TEAM-PENDING") + }) + + test("/grok writes state=GROK when activation succeeds", async () => { + await reachQueue() + addBotMessage("The team will reply to your message") + const joinPromise = simulateGrokJoinSuccess() + await bot.onNewChatItems(customerMessage("/grok")) + await joinPromise + await bot.flush() + expect(chat.customData.get(CUSTOMER_GROUP_ID)?.state).toBe("GROK") + }) + + test("/grok from QUEUE reverts state to QUEUE if activation fails", async () => { + await reachQueue() + addBotMessage("The team will reply to your message") + chat.apiAddMemberWillFail() + await bot.onNewChatItems(customerMessage("/grok")) + await bot.flush() + expect(chat.customData.get(CUSTOMER_GROUP_ID)?.state).toBe("QUEUE") + }) + + test("concurrent /team during Grok activation timeout does not demote state", async () => { + await reachQueue() + addBotMessage("The team will reply to your message") + + // Pause activateGrok at waitForGrokJoin so /team can run in the meantime. + // Patching apiAddMember won't work: it's wrapped in withMainProfile's mutex, + // which /team's activateTeam also needs. waitForGrokJoin awaits outside the + // mutex — that's the real race window in production. + let releaseJoin!: (joined: boolean) => void + ;(bot as any).waitForGrokJoin = () => + new Promise((resolve) => { releaseJoin = resolve }) + + // /grok: writes state=GROK optimistically, fire-and-forgets activateGrok. + await bot.onNewChatItems(customerMessage("/grok")) + expect(chat.customData.get(CUSTOMER_GROUP_ID)?.state).toBe("GROK") + + // Let activateGrok progress past apiAddMember into waitForGrokJoin. + await Promise.resolve() + await Promise.resolve() + + // /team while activateGrok is waiting for join — writes TEAM-PENDING + adds members. + await bot.onNewChatItems(customerMessage("/team")) + expect(chat.customData.get(CUSTOMER_GROUP_ID)?.state).toBe("TEAM-PENDING") + expectMemberAdded(CUSTOMER_GROUP_ID, TEAM_MEMBER_1_ID) + + // Simulate Grok join timeout — activateGrok's revertStateOnFail runs. + releaseJoin(false) + await bot.flush() + + // Fix asserts: revert guard sees state != "GROK" and leaves TEAM-PENDING alone. + expect(chat.customData.get(CUSTOMER_GROUP_ID)?.state).toBe("TEAM-PENDING") + }) + + test("first team text writes state=TEAM via gate", async () => { + await reachTeamPending() + addBotMessage("We will reply within 24 hours.") + chat.members.set(CUSTOMER_GROUP_ID, [makeTeamMember(TEAM_MEMBER_1_ID, "Alice")]) + await bot.onNewChatItems(teamMemberMessage("I'll help")) + expect(chat.customData.get(CUSTOMER_GROUP_ID)?.state).toBe("TEAM") + }) +}) + +describe("Card Preview Sender Prefixes", () => { + beforeEach(() => setup()) + + // Helper: extract preview line from card text posted to team group + function getCardPreview(): string { + const teamMsgs = chat.sentTo(TEAM_GROUP_ID) + const cardText = teamMsgs[0] + if (!cardText) return "" + const lines = cardText.split("\n") + // Card layout: header, state, preview, /'join N' — preview is second to last + return lines[lines.length - 2] || "" + } + + test("customer-only messages: first prefixed, rest not", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID, {displayName: "Alice"}) + chat.groups.set(CUSTOMER_GROUP_ID, gi) + addCustomerMessageToHistory("Hello") + addCustomerMessageToHistory("Need help") + await cards.createCard(CUSTOMER_GROUP_ID, gi) + const preview = getCardPreview() + expect(preview).toContain("Alice: Hello") + expect(preview).toContain("!3 /! Need help") + // Second message must NOT have prefix (same sender) + expect(preview).not.toContain("Alice: Need help") + }) + + test("three consecutive customer messages: only first gets prefix", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID, {displayName: "Alice"}) + chat.groups.set(CUSTOMER_GROUP_ID, gi) + addCustomerMessageToHistory("First") + addCustomerMessageToHistory("Second") + addCustomerMessageToHistory("Third") + await cards.createCard(CUSTOMER_GROUP_ID, gi) + const preview = getCardPreview() + const prefixCount = (preview.match(/Alice:/g) || []).length + expect(prefixCount).toBe(1) + expect(preview).toContain("Alice: First") + }) + + test("alternating customer and Grok: each sender change triggers prefix", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID, {displayName: "Alice"}) + chat.groups.set(CUSTOMER_GROUP_ID, gi) + addCustomerMessageToHistory("How does encryption work?") + addGrokMessageToHistory("SimpleX uses double ratchet") + addCustomerMessageToHistory("And metadata?") + await cards.createCard(CUSTOMER_GROUP_ID, gi) + const preview = getCardPreview() + expect(preview).toContain("Alice: How does encryption work?") + expect(preview).toContain("Grok: SimpleX uses double ratchet") + expect(preview).toContain("Alice: And metadata?") + }) + + test("Grok identified by grokContactId, not by display name", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID, {displayName: "Alice"}) + chat.groups.set(CUSTOMER_GROUP_ID, gi) + // Grok message uses GROK_CONTACT_ID → labeled "Grok" regardless of memberProfile + addGrokMessageToHistory("I am Grok") + await cards.createCard(CUSTOMER_GROUP_ID, gi) + const preview = getCardPreview() + expect(preview).toContain("Grok: I am Grok") + }) + + test("team member messages use their memberProfile displayName", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID, {displayName: "Alice"}) + chat.groups.set(CUSTOMER_GROUP_ID, gi) + addCustomerMessageToHistory("Help please") + // Add team member message with explicit display name + const teamCi = makeChatItem({ + dir: "groupRcv", text: "On it!", + memberId: `team-${TEAM_MEMBER_1_ID}`, memberContactId: TEAM_MEMBER_1_ID, + memberDisplayName: "Bob", + }) + const items = chat.chatItems.get(CUSTOMER_GROUP_ID) || [] + items.push(teamCi) + chat.chatItems.set(CUSTOMER_GROUP_ID, items) + await cards.createCard(CUSTOMER_GROUP_ID, gi) + const preview = getCardPreview() + expect(preview).toContain("Alice: Help please") + expect(preview).toContain("Bob: On it!") + }) + + test("bot messages (groupSnd) excluded from preview", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID, {displayName: "Alice"}) + chat.groups.set(CUSTOMER_GROUP_ID, gi) + addCustomerMessageToHistory("Hello") + addBotMessage("The team will reply to your message") + addCustomerMessageToHistory("Thanks") + await cards.createCard(CUSTOMER_GROUP_ID, gi) + const preview = getCardPreview() + expect(preview).not.toContain("The team will reply to your message") + // Both customer messages are from the same sender — only first prefixed + expect(preview).toContain("Alice: Hello") + expect(preview).toContain("!3 /! Thanks") + }) + + test("media-only message shows type label", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID, {displayName: "Alice"}) + chat.groups.set(CUSTOMER_GROUP_ID, gi) + const imgCi = makeChatItem({dir: "groupRcv", text: "", memberId: CUSTOMER_ID, msgType: "image"}) + const items = chat.chatItems.get(CUSTOMER_GROUP_ID) || [] + items.push(imgCi) + chat.chatItems.set(CUSTOMER_GROUP_ID, items) + await cards.createCard(CUSTOMER_GROUP_ID, gi) + const preview = getCardPreview() + expect(preview).toContain("[image]") + }) + + test("media message with caption shows label + text", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID, {displayName: "Alice"}) + chat.groups.set(CUSTOMER_GROUP_ID, gi) + const imgCi = makeChatItem({dir: "groupRcv", text: "screenshot of the bug", memberId: CUSTOMER_ID, msgType: "image"}) + const items = chat.chatItems.get(CUSTOMER_GROUP_ID) || [] + items.push(imgCi) + chat.chatItems.set(CUSTOMER_GROUP_ID, items) + await cards.createCard(CUSTOMER_GROUP_ID, gi) + const preview = getCardPreview() + expect(preview).toContain("[image] screenshot of the bug") + }) + + test("long message truncated with [truncated]", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID, {displayName: "Alice"}) + chat.groups.set(CUSTOMER_GROUP_ID, gi) + const longMsg = "x".repeat(300) + addCustomerMessageToHistory(longMsg) + await cards.createCard(CUSTOMER_GROUP_ID, gi) + const preview = getCardPreview() + expect(preview).toContain("[truncated]") + // Truncated at ~200 chars + prefix + expect(preview.length).toBeLessThan(300) + }) + + test("total overflow truncates oldest messages, keeps newest", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID, {displayName: "Alice"}) + chat.groups.set(CUSTOMER_GROUP_ID, gi) + // Add many messages to exceed 1000 chars total + for (let i = 0; i < 20; i++) { + addCustomerMessageToHistory(`Message number ${i} with some extra padding text to fill space quickly`) + } + await cards.createCard(CUSTOMER_GROUP_ID, gi) + const preview = getCardPreview() + expect(preview).toContain("[truncated]") + // Newest messages should be present, oldest truncated + expect(preview).toContain("Message number 19") + expect(preview).not.toContain("Message number 0") + // Should not include all 20 messages + const slashCount = (preview.match(/ \/ /g) || []).length + expect(slashCount).toBeLessThan(19) + }) + + test("empty preview when no messages", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID, {displayName: "Alice"}) + chat.groups.set(CUSTOMER_GROUP_ID, gi) + await cards.createCard(CUSTOMER_GROUP_ID, gi) + const preview = getCardPreview() + expect(preview).toBe('""') + }) + + test("only bot messages → empty preview", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID, {displayName: "Alice"}) + chat.groups.set(CUSTOMER_GROUP_ID, gi) + addBotMessage("Welcome!") + addBotMessage("Queue message") + await cards.createCard(CUSTOMER_GROUP_ID, gi) + const preview = getCardPreview() + expect(preview).toBe('""') + }) + + test("newlines in message text → replaced with spaces", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID, {displayName: "Alice"}) + chat.groups.set(CUSTOMER_GROUP_ID, gi) + addCustomerMessageToHistory("line1\nline2\n\nline3") + await cards.createCard(CUSTOMER_GROUP_ID, gi) + const preview = getCardPreview() + expect(preview).not.toContain("\n") + expect(preview).toContain("line1 line2 line3") + }) + + test("newlines in customer display name → sanitized in card header", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID, {displayName: "First\nLast"}) + chat.groups.set(CUSTOMER_GROUP_ID, gi) + addCustomerMessageToHistory("Hello") + await cards.createCard(CUSTOMER_GROUP_ID, gi) + const teamMsgs = chat.sentTo(TEAM_GROUP_ID) + expect(teamMsgs.length).toBe(1) + const cardText = teamMsgs[0] + // Card header should have sanitized name (no newlines) + expect(cardText).toContain("First Last") + // Exactly 4 lines: header, state, preview, /'join N' + expect(cardText.split("\n").length).toBe(4) + expect(cardText).toContain(`/'join ${CUSTOMER_GROUP_ID}'`) + }) +}) + +describe("Restart Card Recovery", () => { + beforeEach(() => setup()) + + test("refreshAllCards refreshes groups with active cards", async () => { + const GROUP_A = 101 + const GROUP_B = 102 + const GROUP_NO_CARD = 103 + chat.groups.set(GROUP_A, makeGroupInfo(GROUP_A)) + chat.groups.set(GROUP_B, makeGroupInfo(GROUP_B)) + chat.groups.set(GROUP_NO_CARD, makeGroupInfo(GROUP_NO_CARD)) + chat.customData.set(GROUP_A, {cardItemId: 501}) + chat.customData.set(GROUP_B, {cardItemId: 503}) + + await cards.refreshAllCards() + + expectCardDeleted(501) + expectCardDeleted(503) + expect(chat.sentTo(TEAM_GROUP_ID).length).toBe(2) // 2 cards × 1 message each + }) + + test("refreshAllCards with no active cards → no-op", async () => { + await cards.refreshAllCards() + expect(chat.deleted.length).toBe(0) + expect(chat.sentTo(TEAM_GROUP_ID).length).toBe(0) + }) + + test("refreshAllCards ignores groups without cardItemId in customData", async () => { + const GROUP_A = 101 + chat.groups.set(GROUP_A, makeGroupInfo(GROUP_A)) + chat.customData.set(GROUP_A, {someOtherData: true}) + + await cards.refreshAllCards() + expect(chat.deleted.length).toBe(0) + expect(chat.sentTo(TEAM_GROUP_ID).length).toBe(0) + }) + + test("refreshAllCards orders by cardItemId ascending (oldest first, newest last)", async () => { + // GROUP_C has higher cardItemId (more recent) than GROUP_A and GROUP_B + const GROUP_A = 101, GROUP_B = 102, GROUP_C = 103 + chat.groups.set(GROUP_A, makeGroupInfo(GROUP_A)) + chat.groups.set(GROUP_B, makeGroupInfo(GROUP_B)) + chat.groups.set(GROUP_C, makeGroupInfo(GROUP_C)) + chat.customData.set(GROUP_C, {cardItemId: 900}) // newest — should refresh last + chat.customData.set(GROUP_A, {cardItemId: 100}) // oldest — should refresh first + chat.customData.set(GROUP_B, {cardItemId: 500}) // middle + + await cards.refreshAllCards() + + // Verify deletion order: oldest cardItemId first, newest last + expect(chat.deleted.length).toBe(3) + expect(chat.deleted[0].itemIds).toEqual([100]) + expect(chat.deleted[1].itemIds).toEqual([500]) + expect(chat.deleted[2].itemIds).toEqual([900]) + + // Newest card is posted last → appears at bottom of team group + const teamMsgs = chat.sentTo(TEAM_GROUP_ID) + expect(teamMsgs.length).toBe(3) // 3 cards × 1 message each + }) + + test("refreshAllCards skips cards marked complete", async () => { + const GROUP_A = 101, GROUP_B = 102 + chat.groups.set(GROUP_A, makeGroupInfo(GROUP_A)) + chat.groups.set(GROUP_B, makeGroupInfo(GROUP_B)) + chat.customData.set(GROUP_A, {cardItemId: 100, complete: true}) + chat.customData.set(GROUP_B, {cardItemId: 200}) + + await cards.refreshAllCards() + + expect(chat.deleted.length).toBe(1) + expect(chat.deleted[0].itemIds).toEqual([200]) + expect(chat.deleted.some(d => d.itemIds.includes(100))).toBe(false) + }) + + test("refreshAllCards deletes old card before reposting", async () => { + const GROUP_A = 101 + chat.groups.set(GROUP_A, makeGroupInfo(GROUP_A)) + chat.customData.set(GROUP_A, {cardItemId: 501}) + + await cards.refreshAllCards() + + // Old card should be deleted + expect(chat.deleted.length).toBe(1) + expect(chat.deleted[0].itemIds).toEqual([501]) + // New card posted + expect(chat.sentTo(TEAM_GROUP_ID).length).toBe(1) + }) + + test("refreshAllCards ignores delete failure (>24h old card)", async () => { + const GROUP_A = 101 + chat.groups.set(GROUP_A, makeGroupInfo(GROUP_A)) + chat.customData.set(GROUP_A, {cardItemId: 501}) + chat.apiDeleteChatItemsWillFail() + + await cards.refreshAllCards() + + // Delete failed but new card still posted + expect(chat.sentTo(TEAM_GROUP_ID).length).toBe(1) + // customData updated with new cardItemId + const newData = chat.customData.get(GROUP_A) + expect(typeof newData.cardItemId).toBe("number") + expect(newData.cardItemId).not.toBe(501) // new ID, not the old one + }) + + test("card flush writes complete: true for auto-completed conversations", async () => { + const GROUP_A = 101 + chat.groups.set(GROUP_A, makeGroupInfo(GROUP_A)) + chat.members.set(GROUP_A, [makeTeamMember(TEAM_MEMBER_1_ID, "Alice")]) + // Team member message from 4 hours ago (> completeHours=3h) → auto-complete + const oldCi = makeChatItem({dir: "groupRcv", text: "Resolved!", memberId: `team-${TEAM_MEMBER_1_ID}`, memberContactId: TEAM_MEMBER_1_ID}) + oldCi.meta.createdAt = new Date(Date.now() - 4 * 3600_000).toISOString() + chat.chatItems.set(GROUP_A, [oldCi]) + // Create initial card data + chat.customData.set(GROUP_A, {cardItemId: 500}) + + cards.scheduleUpdate(GROUP_A) + await cards.flush() + + const data = chat.customData.get(GROUP_A) + expect(data.complete).toBe(true) + }) + + test("card flush clears complete flag when conversation becomes active again", async () => { + const GROUP_A = 101 + chat.groups.set(GROUP_A, makeGroupInfo(GROUP_A)) + chat.members.set(GROUP_A, [makeTeamMember(TEAM_MEMBER_1_ID, "Alice")]) + // Team member message from 4h ago + recent customer message → NOT complete + const teamCi = makeChatItem({dir: "groupRcv", text: "Resolved!", memberId: `team-${TEAM_MEMBER_1_ID}`, memberContactId: TEAM_MEMBER_1_ID}) + teamCi.meta.createdAt = new Date(Date.now() - 4 * 3600_000).toISOString() + const custCi = makeChatItem({dir: "groupRcv", text: "Actually one more question", memberId: CUSTOMER_ID}) + chat.chatItems.set(GROUP_A, [teamCi, custCi]) + // Previously complete + chat.customData.set(GROUP_A, {cardItemId: 500, complete: true}) + + cards.scheduleUpdate(GROUP_A) + await cards.flush() + + const data = chat.customData.get(GROUP_A) + expect(data.complete).toBeUndefined() + }) + + test("refreshAllCards continues on individual card failure", async () => { + const GROUP_A = 101, GROUP_B = 102 + chat.groups.set(GROUP_A, makeGroupInfo(GROUP_A)) + chat.groups.set(GROUP_B, makeGroupInfo(GROUP_B)) + chat.customData.set(GROUP_A, {cardItemId: 100}) + chat.customData.set(GROUP_B, {cardItemId: 200}) + + chat.apiDeleteChatItemsWillFail() + await cards.refreshAllCards() + expectCardDeleted(200) + }) +}) + +describe("joinedGroupMember Event Filtering", () => { + beforeEach(() => setup()) + + test("joinedGroupMember in non-team group → ignored (no DM)", async () => { + const member = {memberId: "someone", groupMemberId: 9000, memberContactId: null, memberStatus: GroupMemberStatus.Connected, memberProfile: {displayName: "Someone"}} + await bot.onJoinedGroupMember(joinedEvent(CUSTOMER_GROUP_ID, member)) + expect(chat.rawCmds.length).toBe(0) + expect(chat.sent.filter(s => s.chat[0] === ChatType.Direct).length).toBe(0) + }) + + test("joinedGroupMember from wrong user → ignored", async () => { + const member = {memberId: "someone", groupMemberId: 9001, memberContactId: null, memberStatus: GroupMemberStatus.Connected, memberProfile: {displayName: "Someone"}} + await bot.onJoinedGroupMember(joinedEvent(TEAM_GROUP_ID, member, GROK_USER_ID)) + expect(chat.rawCmds.length).toBe(0) + }) +}) + +describe("parseConfig Validation", () => { + const baseArgs = ["--team-group", "Support"] + + test("--complete-hours non-numeric → throws", () => { + expect(() => parseConfig([...baseArgs, "--complete-hours", "abc"])) + .toThrow(/--complete-hours must be a non-negative integer, got "abc"/) + }) + + test("postgres backend without --pg-conn → throws", () => { + const prev = process.env.SIMPLEX_BACKEND + process.env.SIMPLEX_BACKEND = "postgres" + try { + expect(() => parseConfig(baseArgs)) + .toThrow(/--pg-conn is required when backend is postgres/) + } finally { + if (prev === undefined) delete process.env.SIMPLEX_BACKEND + else process.env.SIMPLEX_BACKEND = prev + } + }) + + test("postgres backend with --pg-conn → db is postgres DbConfig", () => { + const prev = process.env.SIMPLEX_BACKEND + process.env.SIMPLEX_BACKEND = "postgres" + try { + const cfg = parseConfig([...baseArgs, "--pg-conn", "postgres://user:pass@localhost/db"]) + expect(cfg.db).toEqual({type: "postgres", connectionString: "postgres://user:pass@localhost/db"}) + } finally { + if (prev === undefined) delete process.env.SIMPLEX_BACKEND + else process.env.SIMPLEX_BACKEND = prev + } + }) + + test("postgres backend with --pg-schema → DbConfig carries schemaPrefix", () => { + const prev = process.env.SIMPLEX_BACKEND + process.env.SIMPLEX_BACKEND = "postgres" + try { + const cfg = parseConfig([...baseArgs, "--pg-conn", "postgres://localhost/db", "--pg-schema", "bot"]) + expect(cfg.db).toEqual({type: "postgres", connectionString: "postgres://localhost/db", schemaPrefix: "bot"}) + } finally { + if (prev === undefined) delete process.env.SIMPLEX_BACKEND + else process.env.SIMPLEX_BACKEND = prev + } + }) + + test("sqlite backend (default) → db is sqlite DbConfig with default filePrefix", () => { + const prevBackend = process.env.SIMPLEX_BACKEND + const prevNpm = process.env.npm_config_simplex_backend + delete process.env.SIMPLEX_BACKEND + delete process.env.npm_config_simplex_backend + try { + const cfg = parseConfig(baseArgs) + expect(cfg.db).toEqual({type: "sqlite", filePrefix: "./data/simplex"}) + } finally { + if (prevBackend !== undefined) process.env.SIMPLEX_BACKEND = prevBackend + if (prevNpm !== undefined) process.env.npm_config_simplex_backend = prevNpm + } + }) + + test("sqlite backend with --sqlite-key → DbConfig carries encryptionKey", () => { + const cfg = parseConfig([...baseArgs, "--sqlite-key", "secret"]) + expect(cfg.db).toEqual({type: "sqlite", filePrefix: "./data/simplex", encryptionKey: "secret"}) + }) + + test("unknown flag → parseArgs throws", () => { + expect(() => parseConfig([...baseArgs, "--team-gropu", "typo"])) + .toThrow() + }) + + test("missing --team-group → throws", () => { + expect(() => parseConfig([])) + .toThrow(/required option '--team-group/) + }) + + test("invalid SIMPLEX_BACKEND → throws", () => { + const prev = process.env.SIMPLEX_BACKEND + process.env.SIMPLEX_BACKEND = "mysql" + try { + expect(() => parseConfig(baseArgs)) + .toThrow(/Invalid SIMPLEX_BACKEND: "mysql"/) + } finally { + if (prev === undefined) delete process.env.SIMPLEX_BACKEND + else process.env.SIMPLEX_BACKEND = prev + } + }) + + test("--complete-hours negative → throws", () => { + // parseArgs refuses "-1" as a bare arg (ambiguous with a short flag), so use `=` form + expect(() => parseConfig([...baseArgs, "--complete-hours", "-1"])) + .toThrow(/--complete-hours must be a non-negative integer, got "-1"/) + }) + + test("--card-flush-seconds non-numeric → throws", () => { + expect(() => parseConfig([...baseArgs, "--card-flush-seconds", "xyz"])) + .toThrow(/--card-flush-seconds must be a non-negative integer, got "xyz"/) + }) + + test("--timezone invalid IANA → throws", () => { + expect(() => parseConfig([...baseArgs, "--timezone", "Not/AZone"])) + .toThrow(/--timezone "Not\/AZone" is not a valid IANA time zone/) + }) + + test("--complete-hours 0 → allowed (disables auto-complete)", () => { + const cfg = parseConfig([...baseArgs, "--complete-hours", "0"]) + expect(cfg.completeHours).toBe(0) + }) + + test("valid IANA timezone → accepted", () => { + const cfg = parseConfig([...baseArgs, "--timezone", "America/New_York"]) + expect(cfg.timezone).toBe("America/New_York") + }) +}) + +describe("GrokApiClient HTTP timeout", () => { + test("chat() calls AbortSignal.timeout(60_000) and passes the signal to fetch", async () => { + const timeoutSpy = vi.spyOn(AbortSignal, "timeout") + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response(JSON.stringify({choices: [{message: {content: "ok"}}]}), {status: 200}), + ) + + const client = new GrokApiClient("test-key", "system prompt") + await client.chat([], "hello") + + expect(timeoutSpy).toHaveBeenCalledWith(60_000) + expect((fetchSpy.mock.calls[0][1] as RequestInit).signal).toBeInstanceOf(AbortSignal) + + fetchSpy.mockRestore() + timeoutSpy.mockRestore() + }) +}) + +// Lazy per-group command sync. sendToGroup always calls +// apiUpdateGroupProfile on the first send per group when the group's +// stored groupPreferences.commands don't match desiredCommands. Each +// group is synced at most once per process (cache hit on subsequent +// sends). +describe("Command sync in sendToGroup", () => { + beforeEach(() => setup()) + + test("first send → apiUpdateGroupProfile called once with merged commands", async () => { + await bot.sendToGroup(CUSTOMER_GROUP_ID, "Hello, just a greeting.") + expect(chat.profileUpdates).toHaveLength(1) + const {groupId, profile} = chat.profileUpdates[0] + expect(groupId).toBe(CUSTOMER_GROUP_ID) + expect(profile.groupPreferences.commands).toEqual(DESIRED_COMMANDS) + // Existing groupProfile fields (displayName, fullName) are preserved. + expect(profile.displayName).toBe(`Group${CUSTOMER_GROUP_ID}`) + expect(profile.fullName).toBe("") + // The actual message still goes out after the sync. + expect(chat.lastSentTo(CUSTOMER_GROUP_ID)).toBe("Hello, just a greeting.") + }) + + test("group already has desired commands → no apiUpdateGroupProfile, but still cached", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID) + gi.groupProfile.groupPreferences = {commands: DESIRED_COMMANDS} + chat.groups.set(CUSTOMER_GROUP_ID, gi) + + await bot.sendToGroup(CUSTOMER_GROUP_ID, "Click /grok for help.") + expect(chat.profileUpdates).toHaveLength(0) + // Cache was populated — a subsequent send even against a divergent DB + // won't re-check. + gi.groupProfile.groupPreferences = {commands: []} + await bot.sendToGroup(CUSTOMER_GROUP_ID, "Send /team for a human.") + expect(chat.profileUpdates).toHaveLength(0) + }) + + test("cache: two sends to same group → sync only once", async () => { + await bot.sendToGroup(CUSTOMER_GROUP_ID, "Click /grok first.") + await bot.sendToGroup(CUSTOMER_GROUP_ID, "Or send /team.") + expect(chat.profileUpdates).toHaveLength(1) + expect(chat.sentTo(CUSTOMER_GROUP_ID)).toHaveLength(2) + }) + + test("independent per group: different groups each sync separately", async () => { + const gId2 = 101 + chat.groups.set(gId2, makeGroupInfo(gId2)) + await bot.sendToGroup(CUSTOMER_GROUP_ID, "Click /grok.") + await bot.sendToGroup(gId2, "Send /team.") + expect(chat.profileUpdates.map(p => p.groupId).sort()).toEqual([CUSTOMER_GROUP_ID, gId2].sort()) + }) + + test("merge preserves existing group preference fields (files, etc.)", async () => { + const gi = makeGroupInfo(CUSTOMER_GROUP_ID) + gi.groupProfile.groupPreferences = { + files: {enable: "on"}, + reactions: {enable: "on"}, + } + chat.groups.set(CUSTOMER_GROUP_ID, gi) + + await bot.sendToGroup(CUSTOMER_GROUP_ID, "Click /grok.") + expect(chat.profileUpdates).toHaveLength(1) + const prefs = chat.profileUpdates[0].profile.groupPreferences + expect(prefs.commands).toEqual(DESIRED_COMMANDS) + expect(prefs.files).toEqual({enable: "on"}) + expect(prefs.reactions).toEqual({enable: "on"}) + }) +}) diff --git a/apps/simplex-support-bot/package-lock.json b/apps/simplex-support-bot/package-lock.json new file mode 100644 index 0000000000..1569c18309 --- /dev/null +++ b/apps/simplex-support-bot/package-lock.json @@ -0,0 +1,2022 @@ +{ + "name": "simplex-chat-support-bot", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "simplex-chat-support-bot", + "version": "0.1.0", + "license": "AGPL-3.0", + "dependencies": { + "@simplex-chat/types": "^0.5.0", + "async-mutex": "^0.5.0", + "commander": "^14.0.3", + "simplex-chat": "^6.5.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.9.3", + "vitest": "^1.6.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@simplex-chat/types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@simplex-chat/types/-/types-0.5.0.tgz", + "integrity": "sha512-f680CRlf+O8WfIaPb7wxVj3PB8mTIOE+HqmetCSe0NBheVAjU3ovg3+zkrWwDlavrHuCLbb7Gmeu4HyNtjDfog==", + "license": "AGPL-3.0", + "dependencies": { + "typescript": "^5.9.2" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", + "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simplex-chat": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/simplex-chat/-/simplex-chat-6.5.0.tgz", + "integrity": "sha512-QFGI734HhYJ7trSrEKiZ2mbodI0V8CLDGEv2+yt5zsg0FqftxSpFik6zUSezTRZtN1M8WmSlT44qlEt2a1fXQw==", + "hasInstallScript": true, + "license": "AGPL-3.0", + "dependencies": { + "@simplex-chat/types": "^0.5.0", + "extract-zip": "^2.0.1", + "fast-deep-equal": "^3.1.3", + "node-addon-api": "^8.5.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/apps/simplex-support-bot/package.json b/apps/simplex-support-bot/package.json new file mode 100644 index 0000000000..0b8a3e25d1 --- /dev/null +++ b/apps/simplex-support-bot/package.json @@ -0,0 +1,23 @@ +{ + "name": "simplex-chat-support-bot", + "version": "0.1.0", + "private": true, + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@simplex-chat/types": "^0.6.0", + "async-mutex": "^0.5.0", + "commander": "^14.0.3", + "simplex-chat": "^6.5.1" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.9.3", + "vitest": "^1.6.1" + }, + "author": "SimpleX Chat", + "license": "AGPL-3.0" +} diff --git a/apps/simplex-support-bot/plans/20260207-support-bot-implementation.md b/apps/simplex-support-bot/plans/20260207-support-bot-implementation.md new file mode 100644 index 0000000000..c3c11ef61f --- /dev/null +++ b/apps/simplex-support-bot/plans/20260207-support-bot-implementation.md @@ -0,0 +1,1471 @@ +# SimpleX Support Bot — Implementation Plan + +## 1. Executive Summary + +SimpleX Chat support bot — standalone Node.js app using `simplex-chat-nodejs` native NAPI binding. Single `ChatApi` instance with two user profiles (main bot + Grok agent) sharing one SQLite database. A `profileMutex` serializes all profile-switching + SimpleX API calls. Team sees active conversations as cards in a dashboard group — no text forwarding. Implements flow: Welcome → Queue → Grok/Team-Pending → Team. + +## 2. Architecture + +``` +┌─────────────────────────────────────────────────┐ +│ Support Bot Process (Node.js) │ +│ │ +│ chat: ChatApi ← ChatApi.init("./data/simplex") │ +│ Single database, two user profiles │ +│ │ +│ mainUserId ← non-Grok user (default name: │ +│ "Ask SimpleX Team") │ +│ • Business address, event routing, state mgmt │ +│ • Controls group membership │ +│ │ +│ grokUserId ← "Grok" profile │ +│ • Joins customer groups as Member │ +│ • Sends Grok responses into groups │ +│ │ +│ profileMutex: serialize apiSetActiveUser + call │ +│ GrokApiClient → api.x.ai/v1/chat/completions │ +└─────────────────────────────────────────────────┘ +``` + +- Single Node.js process, single `ChatApi` instance via native NAPI +- Two user profiles in one database. The main profile is returned directly from `bot.run()`. The Grok profile's `userId` is persisted to `state.json` as `grokUserId` on the first run (when the bot creates it); subsequent runs identify Grok strictly by that persisted ID (never by display name, which a rename would invalidate). The main profile's displayName is set only on fresh-DB user creation (`"Ask SimpleX Team"`) and is never rewritten by bot code thereafter — `bot.run()` is invoked with `updateProfile: false`. Bot commands (`/grok`, `/team`) are never pushed via global `apiUpdateProfile`; instead they sync lazily per-group in `sendToGroup` — the first send to each group triggers `syncGroupCommands(groupId)`, which verifies the group's `groupPreferences.commands` against `desiredCommands` and calls `apiUpdateGroupProfile` if different (scoped broadcast to that group's members only). Subsequent sends to the same group are cache hits. +- `profileMutex` serializes `apiSetActiveUser(userId)` + the subsequent SimpleX API call. Grok HTTP API calls run **outside** the mutex. +- Events delivered for all profiles — routed by `event.user` field (main → main handler, Grok → Grok handler) +- Business address auto-accept creates a group per customer +- Grok is a second profile invited as a Member — appears as a separate participant +- No cross-profile ID mapping needed — Grok profile uses its own local group IDs from its own events + +## 3. Project Structure + +``` +apps/simplex-support-bot/ +├── package.json # deps: simplex-chat, @simplex-chat/types, async-mutex; devDeps: vitest, @types/node +├── tsconfig.json # ES2022, strict, Node16 module resolution +├── vitest.config.ts # test runner config, path aliases for mocks +├── src/ +│ ├── index.ts # Entry: parse config, init instance, run +│ ├── config.ts # CLI arg parsing, ID:name validation, Config type +│ ├── bot.ts # SupportBot class: state derivation, event dispatch, cards +│ ├── cards.ts # Card formatting, debouncing, lifecycle +│ ├── grok.ts # GrokApiClient: xAI API wrapper, system prompt, history +│ ├── messages.ts # All user-facing message templates +│ └── util.ts # isWeekend, profileMutex, logging helpers +├── bot.test.ts # Vitest suite (154 tests, 31 describes) +├── test/ +│ └── __mocks__/ +│ ├── simplex-chat.js # MockChatApi + utility re-exports +│ └── simplex-chat-types.js # enum re-exports for tests +└── data/ # SQLite databases (created at runtime) +``` + +The Grok system-prompt / context file is supplied at runtime via `--context-file ` (see §4). It is not part of the repo tree. + +## 4. Configuration + +**CLI flags:** + +| Flag | Required | Default | Format | Purpose | +|------|----------|---------|--------|---------| +| `--db-prefix` | No | `./data/simplex` | path | Database file prefix (both profiles share it) | +| `--team-group` | Yes | — | `name` | Team group display name (auto-created if absent, resolved by persisted ID on restarts) | +| `--auto-add-team-members` / `-a` | No | `""` | `ID:name,...` | Comma-separated team member contacts. Validated at startup — exits on mismatch. | +| `--context-file` | Required when `GROK_API_KEY` set | — | path | Grok system-prompt file (SimpleX documentation context). `parseConfig` throws if `GROK_API_KEY` is set without this flag. | +| `--timezone` | No | `"UTC"` | IANA tz | For weekend detection (24h vs 48h). Weekend = Sat 00:00 – Sun 23:59 in this tz. `parseConfig` validates the value by constructing a probe `Intl.DateTimeFormat` and throws with a clear error on `RangeError` (invalid IANA zone) — bot exits before init. | +| `--complete-hours` | No | `3` | integer ≥ 0 | Hours of customer inactivity after last team/Grok reply before auto-completing a conversation (✅). `parseConfig` rejects non-numeric, negative, or `NaN` values with a fail-fast error. `0` is allowed and disables auto-complete. | +| `--card-flush-seconds` | No | `300` | integer ≥ 0 | Seconds between card dashboard update flushes. `parseConfig` rejects non-numeric, negative, or `NaN` values with a fail-fast error. `0` is allowed and disables periodic flush (card updates still occur on explicit `scheduleUpdate` callers but never auto-drain). | + +**Env vars:** `GROK_API_KEY` (optional) — xAI API key. If unset or empty, the bot starts with Grok support fully disabled: it logs `"No GROK_API_KEY provided, disabling Grok support"`, skips Grok profile/contact setup and event handler registration, omits `/grok` from the bot command list, drops the `/grok` clause from customer-facing messages, and treats any `/grok` the customer still types as an unknown command. + +**Numeric argument validation:** `parseConfig` MUST validate every numeric flag (`--complete-hours`, `--card-flush-seconds`) using a helper that throws on non-finite or negative results, rather than raw `parseInt`: + +```typescript +function parseNonNegativeInt(raw: string, flag: string): number { + const n = parseInt(raw, 10) + if (!Number.isFinite(n) || n < 0) { + throw new Error(`${flag} must be a non-negative integer, got "${raw}"`) + } + return n +} + +const completeHours = parseNonNegativeInt(optionalArg(args, "--complete-hours", "3"), "--complete-hours") +const cardFlushSeconds = parseNonNegativeInt(optionalArg(args, "--card-flush-seconds", "300"), "--card-flush-seconds") +``` + +Rationale: `parseInt("foo", 10)` returns `NaN`, and `NaN * 3600_000 === NaN`. Every subsequent comparison (`now - lastTeamGrokTime >= completeMs`) is `false`, so the feature silently becomes a no-op — auto-complete never fires, cards never auto-refresh — and the operator has no signal that they typo'd a flag. Failing fast at startup surfaces the typo before customers interact. `0` is explicitly allowed as a valid "disable" setting. + +**Timezone validation:** `parseConfig` MUST validate `--timezone` by constructing a probe `Intl.DateTimeFormat`: + +```typescript +try { + new Intl.DateTimeFormat("en-US", {timeZone: timezone, weekday: "short"}) +} catch (err) { + throw new Error(`--timezone "${timezone}" is not a valid IANA time zone: ${(err as Error).message}`) +} +``` + +Rationale: `isWeekend` is called from `queueMessage` and `teamAddedMessage` — both run on the hot customer message path. `new Intl.DateTimeFormat(..., {timeZone: , ...})` throws `RangeError: Invalid time zone specified` at every call. Without startup validation, a typo in `--timezone` turns every `/grok`, `/team`, or first-customer-message dispatch into an unhandled error that crashes the per-item handler (though the outer try/catch in `onNewChatItems` contains it, customers receive no reply at all). Validating once at startup surfaces the typo in the operator's console before any customer interaction. + +```typescript +interface Config { + dbPrefix: string + teamGroup: {id: number; name: string} // id=0 at parse time, resolved at startup + teamMembers: {id: number; name: string}[] + grokContactId: number | null // always restored from state file at startup (even when Grok API is disabled, so the one-way gate can identify and remove Grok members) + timezone: string + completeHours: number // default 3 + cardFlushSeconds: number // default 300 + contextFile: string | null // path to Grok system-prompt file; required when grokApiKey !== null + grokApiKey: string | null // null when GROK_API_KEY is not set → Grok disabled +} +``` + +**State file** — `{dbPrefix}_state.json` (co-located with DB files): +```json +{"teamGroupId": 123, "grokContactId": 4} +``` + +Only two keys. All other state is persisted in the group's `customData` (per-conversation state, card IDs) or derived from group metadata (`apiListMembers`). Display data like message counts is read from chat history on demand. + +**Grok contact resolution** (state-file lookup always runs; contact establishment only when enabled): +1. Read `grokContactId` from state file → validate via `apiListContacts` → set `config.grokContactId` (this always runs, even when `grokApiKey === null`, so the one-way gate can identify and remove Grok members from groups) +2. If not found and `grokEnabled`: main profile creates one-time invite link, Grok profile connects, wait for a `contactConnected` event filtered by profile identity (60s — see "Grok contact identification" below), persist the resulting `contactId` atomically before proceeding. +3. If unavailable (with Grok otherwise enabled), bot runs but `/grok` returns "temporarily unavailable" +4. If `grokApiKey === null`: the Grok profile is not resolved or created, no invite link is issued — but `config.grokContactId` is still set from the state file if the contact exists. + +### Grok contact identification + +`grokContactId` is written once and used forever — it is the single identifier for every subsequent Grok check (one-way gate, `onMemberConnected` skip, `isGrok` in card rendering). Identification MUST be narrowly scoped so that the `contactId` stored is unambiguously Grok's and no other contact completing a handshake in the 60s establishment window can be latched by mistake. + +Use the predicate form of `ChatApi.wait`. The signature (defined in `node_modules/simplex-chat/src/api.ts:217`) is: + +```typescript +wait( + event: K, + predicate: ((event: ChatEvent & {type: K}) => boolean) | undefined, + timeout: number, +): Promise +``` + +The implementation (api.ts:234) keeps the subscriber attached when the predicate returns `false`, so non-matching events are silently discarded and the wait continues until a matching event arrives or the timeout fires. + +Identification accepts only a `contactConnected` event observed by the MAIN profile (the profile whose `apiCreateLink` issued the invite, and whose `contactId` we persist and later pass to `apiAddMember`) whose connecting contact's profile `displayName` equals the Grok profile's displayName: + +```typescript +const grokProfileName = grokUser.profile.displayName // "Grok" (canonical) +const evt = await chat.wait( + "contactConnected", + (e) => + e.user.userId === mainUser.userId && + e.contact.profile.displayName === grokProfileName, + 60_000, +) +if (!evt) { + console.error(`Timeout waiting for Grok contact (60s, displayName="${grokProfileName}"). ` + + `Check SMP relay availability or re-run after clearing state. Exiting.`) + process.exit(1) +} +config.grokContactId = evt.contact.contactId +state.grokContactId = config.grokContactId +writeState(stateFilePath, state) // atomic: tmp-file + rename (see §13 state persistence) +log(`Grok contact established: ID=${config.grokContactId} (displayName="${grokProfileName}")`) +``` + +Filter rationale: +- `e.user.userId === mainUser.userId` selects the main profile's view of the handshake. Both profiles observe the handshake (the Grok-side event describes the main profile as the `contact`); only the main-side event carries the `contactId` we need for subsequent `apiAddMember` calls. +- `e.contact.profile.displayName === grokProfileName` accepts only the contact whose profile matches the Grok profile just created/updated. This rejects stray inbound contacts (late business-request acceptance, operator test DM, a reconnect of an existing contact) that may complete in the same 60s window. The displayName is read from `evt.contact.profile`, which is `LocalProfile` (see `@simplex-chat/types/src/types.ts:2867`). + +`grokProfileName` is captured from `grokUser.profile.displayName` immediately before the wait, so whichever name the Grok profile was created/updated with earlier in startup is the exact string matched here. + +Single-tenant deployment caveat: if a human contact happens to set its SimpleX displayName to the literal `"Grok"` and completes a handshake with the main profile in the 60s window, the displayName filter alone cannot distinguish them. MVP is single-tenant and Grok's profile is created by the bot itself, so this is not expected in practice; deployments that need stronger guarantees can add a second filter (e.g. `e.contact.profile.image === grokImage` — the bot knows the exact image bytes it assigned to the Grok profile). + +Persistence: `writeState` is atomic (tmp-file + `fs.renameSync`, see §13 "State persistence") so a crash between identification and persistence cannot corrupt the state file. `state.grokContactId` is flushed to disk BEFORE proceeding to bot event wiring — if the process dies after wiring but before persistence, the next startup would issue a second invite link and leave the first Grok contact orphaned in the database. + +**Team group resolution** (auto-create): +1. Read `teamGroupId` from state file → validate via group list +2. If not found: create with `apiNewGroup`, persist new group ID +3. If found: compare `fullGroupPreferences` (directMessages, fullDelete, commands) and displayName with desired values. Only call `apiUpdateGroupProfile` if something differs — avoids unnecessary SMP relay round-trips on every restart. + +**Team group invite link lifecycle:** +1. Delete stale link (best-effort), create new link, print to stdout. Creation is best-effort — if the SMP relay is unreachable, the error is logged and the bot continues without an invite link. The 10-minute deletion timer is only scheduled if creation succeeded. +2. Delete after 10 minutes. On SIGINT/SIGTERM, delete before exit. Deletion must go through `profileMutex` with `apiSetActiveUser(mainUserId)` — the active user may be the Grok profile at the time the timer fires or the signal arrives. + +**Team member validation:** +- If `--auto-add-team-members` (`-a`) provided: validate each contact ID/name pair, fail-fast on mismatch +- If not provided: `/team` tells customers "no team members available yet" + +## 5. State Derivation (Stateless) + +Per-conversation state is stored in the group's `customData` and written at the moment the bot handles each transition (customer's first message, `/grok`, `/team`, team member's first message). On subsequent events `deriveState` returns the stored state as-is — composition changes (team members leaving, Grok leaving) do **not** demote the stored state. The customer's mode (e.g. "waiting for a team response") is meaningful even when no team member is currently present; keeping the state preserves that. Composition is read only by specific handlers (e.g. the `/team` duplicate-invite guard). No chat-history scans for state decisions. No in-memory conversations map — survives restarts. + +**WELCOME detection:** customData has no `state` field until the bot handles the first transition. `deriveState` returns `WELCOME` precisely when `customData.state` is absent. + +**Type vs. persisted state.** The `ConversationState` union in `cards.ts` enumerates all five conceptual states (`WELCOME | QUEUE | GROK | TEAM-PENDING | TEAM`) so event handlers and composition can reason about them uniformly. However, `WELCOME` is NEVER written to `customData.state` — the runtime invariant is "persisted state ∈ {QUEUE, GROK, TEAM-PENDING, TEAM}; absence of the `state` field derives as WELCOME". The `isConversationState` guard in `cards.ts` rejects `WELCOME` on read to preserve this invariant (any stale `state: "WELCOME"` from a crashed transition is treated as absent). Do NOT introduce a separate `PersistedState` type in MVP — the invariant is small enough to enforce at two choke points: `getRawCustomData` on read and the dispatch handlers on write. + +**State-write matrix:** + +| Bot-observed event | `customData.state` written | +|---|---| +| *(initial — no customData yet)* | *(absent ⇒ WELCOME)* | +| Customer's first non-command message | `QUEUE` | +| `/grok` handled — Grok invited | `GROK` | +| `/team` handled — team members added (written at handler time; does not wait for team acceptance) | `TEAM-PENDING` | +| First team-member text message observed | `TEAM` | + +**State is authoritative and monotonic.** Once written, `customData.state` persists across member leave/join events. The only path that clears it is the existing `onLeftMember` handler when the customer themselves leaves — at that point the entire customData is cleared. + +**Failure-path revert is CAS-guarded.** `activateGrok` runs fire-and-forget, so its `setStateOnFail` revert (`QUEUE`) can race with a concurrent transition (e.g. `/team` writing `TEAM-PENDING` while `waitForGrokJoin` is pending). To preserve monotonicity, `revertStateOnFail` is a compare-and-set: it only writes `setStateOnFail` if `customData.state === "GROK"` (the optimistic value both call sites write before invoking `activateGrok`). If another handler has since stamped a different state, the revert is skipped — the in-flight transition wins and stays. + +TEAM-PENDING takes priority over GROK when both Grok and team are present (after `/team` but before team member's first message). `/grok` remains available in TEAM-PENDING — if Grok is not yet in the group, it gets invited; if already present, the command is ignored. + +**State derivation helpers:** +- `getGroupComposition(groupId)` → `{grokMember, teamMembers}` from `apiListMembers` — used for card rendering and the `/team` duplicate-invite guard. +- `deriveState(groupId)` → reads `customData.state`. Returns `WELCOME` iff `customData.state` is absent. No composition lookup. +- `getLastCustomerMessageTime(groupId)` / `getLastTeamOrGrokMessageTime(groupId)` → chat-history timestamp reads used by the card renderer for wait-time and auto-complete only (display, not state). + +**Transitions:** +``` +WELCOME ──(1st msg)──────> QUEUE (send queue msg, create card 🆕) +WELCOME ──(/grok 1st)────> GROK (skip queue msg, create card 🤖) +WELCOME ──(/team 1st)────> TEAM-PENDING (skip queue msg, add team members, create card 👋) +QUEUE ──(/grok)──────────> GROK (invite Grok, update card) +QUEUE ──(/team)──────────> TEAM-PENDING (add team members, update card) +GROK ──(/team)───────────> TEAM-PENDING (add all team members, Grok stays, update card) +GROK ──(user msg)────────> GROK (Grok responds, update card) +TEAM-PENDING ──(/grok)───> invite Grok if not present, else ignore (state stays TEAM-PENDING) +TEAM-PENDING ──(/team)───> reply "already invited" (if team members still present; else re-add silently) +TEAM-PENDING ──(team msg)> TEAM (remove Grok, disable /grok permanently, update card) +TEAM ──(/grok)───────────> reply "team mode", stay TEAM +``` + +## 6. Card-Based Dashboard + +The team group is a live dashboard. The bot maintains exactly one message ("card") per active customer conversation. Cards are deleted and reposted on changes — the group is always a current snapshot. + +### Card format + +Card is a single message. The join command is the final line of the card text — there is no separate join message. + +``` +[ICON] *[Customer Name]* · [wait] · [N msgs] +[STATE][· agent1, agent2, ...] +"[last message(s), truncated]" +/'join [id]' +``` + +**Icons:** + +| Icon | Condition | +|------|-----------| +| 🆕 | QUEUE — first message < 5 min ago | +| 🟡 | QUEUE — waiting < 2 h | +| 🔴 | QUEUE — waiting > 2 h | +| 🤖 | GROK — Grok handling | +| 👋 | TEAM — team added, no reply yet | +| 💬 | TEAM — team has replied, conversation active (customer replied after team) | +| ⏰ | TEAM — customer follow-up unanswered > 2 h | +| ✅ | Done — no customer reply for `completeHours` (default 3h) after last team/Grok message | + +**State labels:** `Queue`, `Grok`, `Team – pending`, `Team` + +**Agents:** comma-separated display names of team members in the group. Omitted when none. + +**Message count:** All messages in chat history except the bot's own (`groupSnd` from main profile). + +**Message preview:** Last several messages, most recent last, separated by ` / `. Newlines in message text are replaced with spaces to prevent card layout bloat from spam. The customer's display name is sanitized (newlines → spaces) for the card header; the `/join` command embeds only the numeric group id. Newest messages are prioritized — when the total exceeds ~500 chars (`maxTotal = 500` in `composeCard`), the oldest messages are truncated (with `[truncated]` prepended) while the newest are always shown. When truncation occurs, the first visible message is guaranteed to have a sender prefix even if it was a continuation in the original sequence. Each message is prefixed with the sender's name (`Name: message`) on the first message in a consecutive run from that sender - subsequent messages from the same sender omit the prefix until a different sender's message appears. Sender identification: Grok contact is detected by `grokContactId` and labeled "Grok"; the customer is identified by matching `memberId` to the group's `customerId` and labeled with their display name; all other members use their `memberProfile.displayName`. Bot's own messages (`groupSnd`) are excluded. Each message truncated to ~200 chars. Media-only messages show type labels: `[image]`, `[file]`, `[voice]`, `[video]`. + +**Join command:** the final line of the card renders as `/'join '` where `` is the customer group's numeric ID. The outer single quotes around `join ` are rendered by SimpleX clients as a clickable quoted command; tapping it sends `/join ` back to the team group. The handler does not pattern-match the message text — it uses the framework's structured command parser (`util.ciBotCommand`) which returns `{keyword: "join", params: ""}` directly from the chat item. The handler then converts `params` to an integer via `Number.parseInt(params, 10)` and rejects anything that is not a positive integer. There is no legacy `/join :` form — the card never emits it, so the handler never needs to strip it. + +### Card lifecycle + +**Tracking:** `{state, cardItemId, complete?}` stored in customer group's `customData` via `apiSetGroupCustomData`. `state` is the canonical conversation state (`QUEUE | GROK | TEAM-PENDING | TEAM`); `cardItemId` is the team-group chat item ID for the (single) card message; `complete` flags the auto-completed state. Absence of `state` means WELCOME. Written at event time by the dispatch handlers — `/grok` handler writes `GROK` on invite; `/team` handler writes `TEAM-PENDING` immediately (does not wait for team acceptance); first observed team-member text message writes `TEAM`; first customer text message writes `QUEUE`. Read back from `groupInfo.customData` — single source of truth, survives restarts. All writes go through `CardManager.mergeCustomData` to preserve fields across independent write paths. + +**Create** — on first customer message (→ QUEUE) or `/grok` as first message (→ GROK): +1. Compose card text (including the `/'join '` final line) +2. Post it via `apiSendMessages(chatRef, [{msgContent: {type: "text", text}, mentions: {}}])` → get one `chatItemId`. The card is a single message; the `/'join '` line is clickable because SimpleX clients render the slash-prefixed single-quoted token as a clickable command even inside a multi-line message. +3. Write `{cardItemId}` to customer group's `customData` + +**Update** (delete + repost) — on every subsequent event (new customer msg, team/Grok reply, state change, agent join): +1. Read `{cardItemId}` from `customData` +2. Delete old card via `apiDeleteChatItems([Group, teamGroupId], [cardItemId], "broadcast")`. Per `simplex-chat/src/api.ts:436-445` the call either returns `T.ChatItemDeletion[]` (possibly empty if the item no longer exists) or throws `ChatCommandError`. Both outcomes are acceptable: the surrounding `try { ... } catch { /* log and continue */ }` allows execution to proceed whether the item was still present, already gone, or the server returned a transient error. +3. Post new card as a single message via `apiSendMessages` → get new `cardItemId`. **On failure** the partial-failure policy below applies: log, re-queue this groupId into `pendingUpdates`, return without writing `customData`. +4. Write `{cardItemId, complete?}` to `customData` via `mergeCustomData`. **On failure** the tracking-write policy below applies. + +**Debouncing:** Card updates debounced globally — pending changes flushed every `cardFlushSeconds` seconds (default 300, configurable via `--card-flush-seconds`). Within a batch, each group's card reposted at most once with latest state. + +**Wait time rules:** Time since the customer's last unanswered message. For ✅ (auto-completed) conversations, the wait field shows the literal string "done". If customer sends a follow-up, wait time resets to count from that message. + +**Auto-complete:** A conversation is marked ✅ when `completeHours` (default 3h, configurable via `--complete-hours`) have passed since the last team/Grok message **without any customer reply**. The card debounce flush (every 300 seconds / 5 min, configurable via `--card-flush-seconds`) checks elapsed time and transitions to ✅ when the threshold is met. Customer follow-up at any point — including after ✅ — reverts to the derived active icon (👋/💬/⏰ for team states, 🟡/🔴 for queue), and wait time resets from that message. + +**Card icon derivation (TEAM states) — computed at each card render by comparing the timestamps of the most recent customer and team/Grok messages in the group; nothing about the icon is stored:** +``` +Team added, no reply yet → 👋 +Team replied → 💬 +Customer follow-up unanswered >2h → ⏰ +No customer reply for completeHours → ✅ +Customer sends after ✅ → back to 💬 or ⏰ (derived from wait time) +``` + +**Cleanup** — customer leaves: card remains (TBD retention), clear `customData`. + +**Restart recovery:** On startup, `CardManager.refreshAllCards()` lists all groups, finds those with `customData.cardItemId` set and `customData.complete` not set, sorts by `cardItemId` ascending (higher ID = more recently updated), and re-posts them oldest-first so the most recently active cards end up at the bottom of the team group. Completed cards (`complete: true`) and old/pre-bot groups (no `customData`) are skipped. Old card messages are deleted before reposting; deletion failures (e.g., >24h old) are silently ignored. Individual card failures are caught and logged without aborting the batch. + +### Partial-failure and retry policy + +`createCard` and `updateCard` perform a multi-step sequence (delete + send + customData write). To design the correct policy we MUST be explicit about which failures the SimpleX core already handles for us vs. which surface to the bot: + +**SimpleX core semantics** (per `simplex-chat/src/api.ts` JSDoc): +- `apiSendMessages` — "Network usage: background". The call returns `newChatItems` once the chat item is CREATED LOCALLY (written to SQLite) and the SMP broadcast is QUEUED. The core's background machinery retries relay delivery transparently — **the bot never observes a transient relay failure from `apiSendMessages`**. A thrown `ChatCommandError` means the local create step itself failed: permission denied, chat does not exist, invalid content, DB locked/corrupted. +- `apiDeleteChatItems` — "Network usage: background". Same pattern: local delete + queued broadcast + core-managed delivery retry. A thrown error means the local delete step failed (item not found, permission, DB error). +- `apiSetGroupCustomData` — "Network usage: **no**". Pure local SQLite write, no SMP involvement at all. A thrown error means a local DB error. + +Consequence: failures surfaced to the bot are **terminal local errors** (bad state, DB problem, permission change), not transient network blips. Retrying the same operation against the same DB/relay state will usually hit the same error. Retry value comes from the narrow slice of genuinely transient local conditions — a brief SQLite lock held by a concurrent write, a race with group-state mutation elsewhere in the same process — where the next attempt sees a different state. + +This reshapes the policy: the bot does not need aggressive retry for "network" reasons (core handles that), and compensating actions for customData-write failure are rarely useful (if the pure-local customData write fails, the retry's customData write will almost certainly fail for the same reason). The bot needs a light safety net: re-queue on any step failure, let the flush loop try again at most once per `cardFlushSeconds`, and on persistent failure accept that operator intervention is needed. + +Policy (applies to both `createCard` and `updateCard`): + +**Any step fails** — whether step 2 (delete), step 3 (send), or step 4 (customData write): +- Log via `logError` with `{groupId, step, err}` so the operator can diagnose the underlying cause (permission change, DB corruption, bot removed from team group, etc). +- Re-add `groupId` to `pendingUpdates` via `this.scheduleUpdate(groupId)`. +- Return. Do NOT attempt compensating actions (no compensating delete for tracking-write failure — the scenario where send succeeds locally but customData write fails requires the SQLite DB to be healthy-then-unhealthy between two synchronous calls in the same transaction window, which is not a realistic transient state; the retry path handles any resulting duplicate by reading the stale `cardItemId` and deleting it on the next update attempt). + +**Flush dispatch** — the current `flush` loop calls `updateCard` unconditionally and `updateCard` returns early when `customData.cardItemId` is unset. This silently drops the retry path for a failed `createCard` — the group is in `pendingUpdates` but nothing will ever create a card for it. Replace with a single `flushOne(groupId)` that reads `customData` once and dispatches to create or update: + +```typescript +private async flushOne(groupId: number): Promise { + const groupInfo = await this.getGroupInfo(groupId) + if (!groupInfo) return // group deleted + const customData = this.deriveCustomData(groupInfo) + if (customData.complete) return // ✅ conversations don't auto-repost + if (typeof customData.cardItemId === "number") { + await this.updateCard(groupId, groupInfo) + } else { + await this.createCard(groupId, groupInfo) + } +} + +async flush(): Promise { + const groups = [...this.pendingUpdates] + this.pendingUpdates.clear() + for (const groupId of groups) { + try { await this.flushOne(groupId) } + catch (err) { + logError(`flush failed for group ${groupId}`, err) + this.scheduleUpdate(groupId) // re-queue on any thrown error + } + } +} +``` + +Retry behavior for each failure point under this design: + +| Failure point | `customData` after failure | Retry's `flushOne` path | Retry outcome if condition cleared | +|---|---|---|---| +| `createCard` send fails | `cardItemId` absent | create-path | fresh card posted, `customData` written | +| `updateCard` delete fails | old `cardItemId` still set | update-path | delete retried (idempotent — see below) + send + write | +| `updateCard` send fails (delete succeeded) | old (now-deleted) `cardItemId` still set | update-path | delete retried against stale ID — tolerated (see below) — then send + write | +| `updateCard` write fails (send succeeded, duplicate may exist) | old `cardItemId` still set, new card orphaned in team group | update-path | delete retried against stale old ID — tolerated — new card posted, tracking written; **leaked** new card from the failed attempt persists until operator removes it | + +**Delete idempotency on retry** — `apiDeleteChatItems` against already-deleted IDs returns either an empty `ChatItemDeletion[]` or throws `ChatCommandError`. The step-2 `try { ... } catch { logError(...) }` swallows both; execution proceeds to step 3. Do NOT escalate a step-2 error to the partial-failure policy — that would create a retry loop for a permanent condition (items past the 24h deletion window will throw on every retry forever). + +**Persistent failures** — if the underlying condition is not transient (bot removed from team group, DB corruption, permission revoked), every retry hits the same error and the group stays in `pendingUpdates` indefinitely, logging at each flush. MVP accepts this — the operator-visible log stream makes the problem diagnosable. A bounded-retry-with-backoff-and-giveup strategy can be added later without changing the failure-point table above. + +### Card implementation + +```typescript +class CardManager { + private pendingUpdates = new Set() // groupIds with pending updates + private flushInterval: NodeJS.Timeout + + constructor(private chat: ChatApi, private config: Config, private mainUserId: number, + flushIntervalMs = 300 * 1000) { + this.flushInterval = setInterval(() => this.flush(), flushIntervalMs) + this.flushInterval.unref() + } + + scheduleUpdate(groupId: number): void { + this.pendingUpdates.add(groupId) + } + + async createCard(groupId: number, groupInfo: T.GroupInfo): Promise { + const {text} = await this.composeCard(groupId, groupInfo) + // Single-message card — the `/'join '` line is the final line of `text`. + const items = await this.chat.apiSendMessages(chatRef, [ + {msgContent: {type: "text", text}, mentions: {}}, + ]) + await this.chat.apiSetGroupCustomData(groupId, { + cardItemId: items[0].chatItem.meta.itemId, + }) + } + + async flush(): Promise { + const groups = [...this.pendingUpdates] + this.pendingUpdates.clear() + for (const groupId of groups) { + await this.updateCard(groupId) + } + } + + async refreshAllCards(): Promise { + const groups = await this.chat.apiListGroups(mainUserId) + const activeCards = groups + .filter(g => typeof g.customData?.cardItemId === "number" && !g.customData?.complete) + .map(g => ({groupId: g.groupId, cardItemId: g.customData.cardItemId})) + // Sort ascending by cardItemId (higher = more recently updated) + activeCards.sort((a, b) => a.cardItemId - b.cardItemId) + for (const {groupId} of activeCards) { + try { await this.updateCard(groupId) } + catch (err) { logError(`Startup card refresh failed for group ${groupId}`, err) } + } + } + + private async updateCard(groupId: number): Promise { + // Read customData via apiListGroups + const customData = ... // {cardItemId} from groupInfo.customData + if (!customData?.cardItemId) return + // Delete old card message + try { + await this.chat.apiDeleteChatItems(Group, teamGroupId, + [customData.cardItemId], "broadcast") + } catch {} // card may already be deleted + const {text, complete} = await this.composeCard(groupId, groupInfo) + const items = await this.chat.apiSendMessages(chatRef, [ + {msgContent: {type: "text", text}, mentions: {}}, + ]) + const data = { + cardItemId: items[0].chatItem.meta.itemId, + ...(complete ? {complete: true} : {}), + } + await this.chat.apiSetGroupCustomData(groupId, data) + } + + private async composeCard(groupId: number, groupInfo: T.GroupInfo): Promise<{text: string, complete: boolean}> { + // Icon, state, agents, preview (with sender-name prefixes), /'join ' — per spec format + // The final line of `text` is `/'join '` — clickable in SimpleX clients. + // buildPreview(chatItems, customerName, customerId) — prefixes each sender's first message in a run + // Preview messages joined with blue "/" separator: " !3 /! " (SimpleX markdown for blue colored text) + // Message text is escaped via escapeStyledMarkdown() before joining — inserts U+200B after "!" + // when followed by a color trigger (1-6,r,g,b,y,c,m,-) to prevent false markdown interpretation. + // No escape mechanism exists in the SimpleX markdown parser for "!" styled text. + // complete = (icon === "✅") + } +} +``` + +## 7. Bot Initialization + +**Main bot** uses `bot.run()` with `events` parameter: + +```typescript +let supportBot: SupportBot + +const [chat, mainUser, mainAddress] = await bot.run({ + profile: {displayName: "Ask SimpleX Team", fullName: "", image: supportImage}, + dbOpts: {dbFilePrefix: config.dbPrefix}, + options: { + addressSettings: { + businessAddress: true, + autoAccept: true, + welcomeMessage, + }, + commands: [ + {type: "command", keyword: "grok", label: "Ask Grok"}, + {type: "command", keyword: "team", label: "Switch to team"}, + ], + useBotProfile: true, + updateProfile: false, // bot code never rewrites displayName/image/etc. + }, + events: { + acceptingBusinessRequest: (evt) => supportBot?.onBusinessRequest(evt), + newChatItems: (evt) => supportBot?.onNewChatItems(evt), + chatItemUpdated: (evt) => supportBot?.onChatItemUpdated(evt), + chatItemReaction: (evt) => supportBot?.onChatItemReaction(evt), + leftMember: (evt) => supportBot?.onLeftMember(evt), + joinedGroupMember: (evt) => supportBot?.onJoinedGroupMember(evt), + connectedToGroupMember: (evt) => supportBot?.onMemberConnected(evt), + newMemberContactReceivedInv: (evt) => supportBot?.onMemberContactReceivedInv(evt), + contactConnected: (evt) => supportBot?.onContactConnected(evt), + contactSndReady: (evt) => supportBot?.onContactSndReady(evt), + }, +}) +``` + +Note: `/grok` and `/team` are passed in `options.commands` so `bot.run()` has a profile to use when `apiCreateActiveUser` is needed on a fresh DB, but since `updateProfile: false` is set, `bot.run()` never writes the profile on subsequent runs. The user profile's `preferences.commands` is intentionally not pushed globally at startup — broadcasting `XInfo` to every contact is not wanted. Instead, the `SupportBot` takes `desiredCommands` as a constructor argument and syncs commands lazily per-group: `sendToGroup` (`src/bot.ts`) always calls `syncGroupCommands(groupId)` before dispatching the message. That helper reads the group via `apiGetChat(Group, groupId, 0)` (local, no network), and if `groupPreferences.commands` differs from `desiredCommands`, issues `apiUpdateGroupProfile` with the merged profile. `apiUpdateGroupProfile` broadcasts `XGrpInfo`/`XGrpPrefs` to group members only (scoped to the chat audience). Already-synced groups are cached in `syncedGroups: Set` so subsequent sends skip the read entirely — the first send per group costs one local read; every later send is a cache hit. Earlier drafts used a regex on the outgoing text to skip the sync when no command keyword appeared; that optimization was removed because the cache already makes repeated syncs free and the parser was a fragile source of correctness bugs. `/join` is registered as a team group command separately — after team group is resolved, call `apiUpdateGroupProfile(teamGroupId, groupProfile)` with `groupPreferences` including the `/join` command definition. Customer sending `/join` in a customer group → treated as ordinary message (unrecognized command). + +**Grok profile** — resolved from same ChatApi instance. Grok is identified strictly by the `userId` persisted in `state.json`; there is no by-name fallback (a renamed profile would otherwise be silently mistaken): + +```typescript +let grokUser: T.User | null = null +if (state.grokUserId !== undefined) { + const users = await chat.apiListUsers() + grokUser = users.find(u => u.user.userId === state.grokUserId)?.user ?? null + if (!grokUser) { + throw new Error( + `Persisted Grok userId=${state.grokUserId} not found in DB. ` + + `Either restore the user or delete state.json to re-create Grok.` + ) + } +} else { + // First run: create Grok and persist its userId immediately. + grokUser = await chat.apiCreateActiveUser({displayName: "Grok", fullName: "", image: grokImage}) + // apiCreateActiveUser sets Grok as active — switch back to main + await chat.apiSetActiveUser(mainUser.userId) + state.grokUserId = grokUser.userId + writeState(stateFilePath, state) +} + +// Refresh Grok's profile if it has drifted from the canonical values. +const grokProfile = {displayName: "Grok", fullName: "", image: grokImage} +const current = util.fromLocalProfile(grokUser.profile) +if (current.image !== grokProfile.image || current.displayName !== grokProfile.displayName || current.fullName !== grokProfile.fullName) { + await chat.apiSetActiveUser(grokUser.userId) + await chat.apiUpdateProfile(grokUser.userId, grokProfile) + await chat.apiSetActiveUser(mainUser.userId) +} +``` + +**Profile mutex** — all SimpleX API calls go through: + +```typescript +import {Mutex} from "async-mutex" + +const profileMutex = new Mutex() + +async function withProfile(userId: number, fn: () => Promise): Promise { + return profileMutex.runExclusive(async () => { + await chat.apiSetActiveUser(userId) + return fn() + }) +} +``` + +Grok HTTP API calls are made **outside** the mutex to avoid blocking. + +**Per-group customData mutex** — `mergeCustomData` and `clearCustomData` must be serialized per customer group. `mergeCustomData` has two awaits (read via `getRawCustomData` → `apiListGroups`, then write via `apiSetGroupCustomData`); between them the event loop runs, so two concurrent async chains operating on the same `groupId` can both read the same snapshot, both produce a merged object, and the second write clobbers the first's patch. + +Concrete call sites that can overlap on one `groupId`: +- `processMainChatItem` writing `state` transitions (WELCOME→QUEUE, WELCOME→GROK, QUEUE→GROK, one-way gate →TEAM) +- `activateGrok`'s `revertStateOnFail` (fire-and-forget) racing with subsequent customer messages +- `activateTeam` writing `TEAM-PENDING` racing with `/grok` or another `/team` on the same group +- `CardManager.flush → updateCard` writing `{cardItemId, complete}` racing with dispatch writing `state` +- `createCard` writing `{cardItemId}` immediately after dispatch writes `state` + +The CAS-on-state inside `revertStateOnFail` guards only the `state` key — other keys (`cardItemId`, `complete`) can still be lost when spread from a stale snapshot. + +Implementation: + +```typescript +// In CardManager +private customDataMutexes = new Map() + +private getCustomDataMutex(groupId: number): Mutex { + let m = this.customDataMutexes.get(groupId) + if (!m) { m = new Mutex(); this.customDataMutexes.set(groupId, m) } + return m +} + +async mergeCustomData(groupId: number, patch: Partial): Promise { + return this.getCustomDataMutex(groupId).runExclusive(async () => { + const current = (await this.getRawCustomData(groupId)) ?? {} + const merged = {...current, ...patch} + for (const key of Object.keys(merged) as (keyof CardData)[]) { + if (merged[key] === undefined) delete merged[key] + } + await this.withMainProfile(() => this.chat.apiSetGroupCustomData(groupId, merged)) + }) +} + +async clearCustomData(groupId: number): Promise { + return this.getCustomDataMutex(groupId).runExclusive(() => + this.withMainProfile(() => this.chat.apiSetGroupCustomData(groupId)) + ) +} +``` + +Nesting rule: the per-group customData mutex is the **outer** lock; `profileMutex` (via `withMainProfile`) is the **inner** lock. Never acquire them in the opposite order, and never hold the customData mutex while calling an external (non-SimpleX) async function — this prevents cross-group deadlock and keeps the critical section short. + +Cleanup: entries in `customDataMutexes` are bounded by the number of customer groups. Removing the entry on `onLeftMember(customer)` is sufficient (the group's `customData` is also cleared at that point). Skip this refinement in MVP if acceptable — a long-running bot with many customers accumulates a few bytes per group. + +**Profile images:** Both profiles have base64-encoded JPEG profile pictures (128x128, quality 85, under the 12,500-char data URI limit enforced by iOS/Android clients) set via the `image` field in `T.Profile`. The images are defined as `data:image/jpg;base64,...` string constants in `index.ts`. The main profile image is passed to `bot.run()` which handles update-on-change automatically. The Grok profile image is passed to `apiCreateActiveUser()` on first run; on subsequent runs, the bot compares the current profile against the desired one using `util.fromLocalProfile()` and calls `apiUpdateProfile()` if any field differs — this sends the update to all Grok contacts. + +**Startup sequence:** +0. **Active user recovery + name preservation:** Two related safeguards. + + **(a) Active user recovery.** On restart, the active user may be Grok (if the previous run was killed mid-profile-switch). `bot.run()` uses `apiGetActiveUser()` and would then operate against Grok's `userId` as if it were the main user. Fix: when `state.grokUserId` is set (i.e. this is not the very first run), pre-init the DB with a temporary `ChatApi` and compare the active user's `userId` against `state.grokUserId`. If they match, `apiListUsers()` + `apiSetActiveUser()` to the single non-Grok user — throw loudly if zero or multiple candidates exist, rather than silently picking. Close the temporary `ChatApi` before `bot.run()` reopens it. Identification is by userId, never by display name; a renamed Grok profile would defeat name matching. + + **(b) Never rewrite the main profile.** The core auto-creates a preset contact named `"Ask SimpleX Team"` in every user's DB (`src/Simplex/Chat/Library/Internal.hs:2749`, exact name from commit `362bdc328` 2025-07-12). That collides with the bot's preferred main-profile displayName within the user's `display_names` namespace (`UNIQUE (user_id, local_display_name)`), so any attempt to rename the main profile to `"Ask SimpleX Team"` fails with `duplicateName`. Worse, `bot.run`'s internal `updateBotUserProfile` (`packages/simplex-chat-nodejs/dist/bot.js:176`) re-syncs image, preferences, and `contactLink` on every startup, and on a DB where `users.local_display_name` has drifted from `contact_profiles.display_name`, the fast path (`src/Simplex/Chat/Store/Profiles.hs:311`) silently rewrites the customer-facing `contact_profiles.display_name`. Fix: pass `options.updateProfile: false` to `bot.run()` so the bot code never calls `apiUpdateProfile` on its own initiative. Whatever displayName the CLI saw is what stays. + + **(c) Lazy per-group command sync.** The bot's command list (`/grok`, `/team`) is synced lazily and per-group, not globally. `sendToGroup` (in `src/bot.ts`) unconditionally calls `syncGroupCommands(groupId)` before dispatching the message. That helper uses `apiGetChat(Group, groupId, 0)` (local DB read, no network) to read the current `groupProfile.groupPreferences.commands`, and if it doesn't match `desiredCommands`, issues `apiUpdateGroupProfile` with the commands merged in. `apiUpdateGroupProfile` broadcasts `XGrpInfo`/`XGrpPrefs` to group members only — scoped to the chat audience, never the whole contact list. Groups confirmed in-sync are cached in `syncedGroups: Set` so the first send per group costs one local read; every later send is a cache hit. No `apiUpdateProfile` (global XInfo broadcast) is ever invoked by bot code. Earlier drafts gated the sync behind a regex match on the outgoing text (to skip the read when no `/keyword` appeared); that optimization was removed because the cache already made repeated syncs free and the parser was a fragile source of correctness bugs. +1. `bot.run()` → init ChatApi, create/resolve main profile (with profile image), business address. Print business address link to stdout. +2. Resolve Grok profile: if `state.grokUserId` is set, look it up by ID via `apiListUsers()` (throw if missing); otherwise create via `apiCreateActiveUser()` and persist the new `userId`. Then compare the resolved profile against the canonical `{displayName, fullName, image}` and call `apiUpdateProfile()` if anything changed — pushes to Grok's contacts. +3. Read `{dbPrefix}_state.json` for `teamGroupId` and `grokContactId` +4. Enable auto-accept DM contacts: `apiSetAutoAcceptMemberContacts(mainUser.userId, true)` +5. List contacts, resolve Grok contact (from state or auto-establish) +6. Resolve team group (from state or auto-create) +7. Ensure direct messages + delete for everyone enabled on team group (conditional — only updates profile if preferences or name differ from desired) +8. Create team group invite link (best-effort), schedule 10min deletion if created +9. Validate `--auto-add-team-members` (`-a`) if provided +10. Register Grok event handlers on `chat` (filtered by `event.user === grokUserId`) +10b. Refresh stale cards: `CardManager.refreshAllCards()` — lists all groups, skips those with `customData.complete` or no `customData.cardItemId`, sorts remaining by `cardItemId` ascending, re-posts oldest-first so newest cards land at the bottom of team group +11. On SIGINT/SIGTERM → `clearTimeout(inviteLinkTimer)` (noop if already deleted), `cards.destroy()` (stops the card-flush interval), `deleteInviteLink()` (profileMutex-gated `apiDeleteGroupLink`), `process.exit(0)`. Signal handler is reentrant-safe: an `inviteLinkDeleted` flag prevents double-deletion; `clearTimeout`/`clearInterval` are no-op on undefined. + +**Grok event registration** (same ChatApi, filtered by profile): + +```typescript +chat.on("receivedGroupInvitation", async (evt) => { + if (evt.user.userId !== grokUserId) return + supportBot?.onGrokGroupInvitation(evt) +}) +chat.on("newChatItems", async (evt) => { + if (evt.user.userId !== grokUserId) return + supportBot?.onGrokNewChatItems(evt) +}) +chat.on("connectedToGroupMember", (evt) => { + if (evt.user.userId !== grokUserId) return + supportBot?.onGrokMemberConnected(evt) +}) +``` + +## 8. Event Processing + +**Main profile event handlers:** + +| Event | Handler | Action | +|-------|---------|--------| +| `acceptingBusinessRequest` | `onBusinessRequest` | Enable file uploads + visible history on business group | +| `newChatItems` | `onNewChatItems` | Route: team group → handle `/join`; customer group → derive state, dispatch; direct message → reply with business address link | +| `chatItemUpdated` | `onChatItemUpdated` | Schedule card update | +| `leftMember` | `onLeftMember` | Customer left → cleanup, card remains. Grok left → cleanup. Team member left → revert if no message sent. | +| `joinedGroupMember` | `onJoinedGroupMember` | Team group joiner (link-join): initiate DM via `apiCreateMemberContact` + `apiSendMemberContactInvitation`. Fires for any member joining via group invite link. | +| `connectedToGroupMember` | `onMemberConnected` | In team group: send DM with contact ID (if not already sent by `onJoinedGroupMember`). In customer group: promote to Owner (unless customer or Grok). | +| `chatItemReaction` | `onChatItemReaction` | Team/Grok reaction in customer group → schedule card update (auto-complete) | +| `newMemberContactReceivedInv` | `onMemberContactReceivedInv` | Team group member DM contact received: send contact ID message immediately (dedup via `sentTeamDMs`) | +| `contactConnected` | `onContactConnected` | Deliver pending DM if queued (dedup via `sentTeamDMs`) | +| `contactSndReady` | `onContactSndReady` | Deliver pending DM if queued (dedup via `sentTeamDMs`) | + +**Grok profile event handlers:** + +| Event | Handler | Action | +|-------|---------|--------| +| `receivedGroupInvitation` | `onGrokGroupInvitation` | Look up `pendingGrokJoins`; if found, auto-accept via `apiJoinGroup`; if not found (race), buffer in `bufferedGrokInvitations` for `activateGrok` to drain | +| `connectedToGroupMember` | `onGrokMemberConnected` | Grok now fully connected — read last 100 msgs from own view, call Grok API, send initial response | +| `newChatItems` | `onGrokNewChatItems` | Batch dedup: collect last customer text message per group in the event. Skip groups with `grokInitialResponsePending` set (initial combined response in flight). For the selected message: read last 100 msgs, call Grok API, send response. Non-text (images, files, voice) → ignored by Grok (card update handled by main profile). | + +**Message routing in `onNewChatItems` (main profile):** + +```typescript +// For each chatItem: +// 1. Direct message (not group) → reply with business address link, stop +// 2. Team group (groupId === teamGroupId) → handle /join command +// 3. Skip non-business-chat groups +// 4. Skip groupSnd (own messages) +// 5. Identify sender via businessChat.customerId +// 6. Team member message → check if first team text (trigger one-way gate: remove Grok, disable /grok), schedule card update +// 7. Customer message → derive state, dispatch: +// - WELCOME: create card, send queue msg (or handle /grok first msg → WELCOME→GROK, skip queue) +// - QUEUE: /grok → invite Grok; /team → add ALL configured team members; else schedule card update +// - GROK: /team → add ALL configured team members (Grok stays); else schedule card update +// - TEAM-PENDING: /grok → invite Grok if not present, else ignore; /team → if team members still present, reply "already invited"; if all team members have left, re-add silently (state stays TEAM-PENDING); else no action +// - TEAM: /grok → reply "team mode"; else no action +``` + +## 9. One-Way Gate + +The gate is event-driven and persists its transitions. The initial `/team` guard reads `customData.state` AND group composition: if state is already `TEAM-PENDING`/`TEAM` **and** team members are still present, the bot replies `teamAlreadyInvitedMessage` without re-adding. If state is `TEAM-PENDING`/`TEAM` but all team members have left, the bot re-adds them (state stays `TEAM-PENDING`). The first-team-message detection writes `state: 'TEAM'` into customData at the moment the bot observes the message, then removes Grok and disables `/grok`. + +1. User sends `/team` → ALL configured `--auto-add-team-members` (`-a`) added to group (each promoted to Owner at invite time via `apiSetMembersRole`, re-asserted on connect as fallback) → Grok stays if present → TEAM-PENDING +2. Repeat `/team` → detected via `customData.state ∈ {TEAM-PENDING, TEAM}` **and team members still present** → reply with `teamAlreadyInvitedMessage`. If team members have since left, re-add them silently (state stays `TEAM-PENDING`). +3. `/grok` still works in TEAM-PENDING (if Grok not present, invite it; if present, ignore — Grok responds to customer messages) +4. Any team member sends first text message in customer group → **gate triggers**: + - Remove Grok from group (`apiRemoveMembers`) + - `/grok` permanently disabled → replies: "You are now in team mode. A team member will reply to your message." + - State = `TEAM` (written as `customData.state = 'TEAM'` at observation time) +5. Detection: in `onNewChatItems`, when sender is a team member and `customData.state !== 'TEAM'`, trigger the gate and write `state: 'TEAM'` via `mergeCustomData`. + +**Edge cases:** +- All team members leave before sending → state stays `TEAM-PENDING` (customer is still waiting for a response); sending `/team` re-adds them without the "already invited" reply. +- Team member leaves after sending → state stays `TEAM` (`customData.state` persists). Customer can send `/team` again to re-add team members. + +## 10. Grok Integration + +Grok is a **second user profile** in the same ChatApi instance. Self-contained: watches its own events, reads history from its own view, calls Grok HTTP API, sends responses. + +### Grok-disabled mode (no `GROK_API_KEY`) + +If `GROK_API_KEY` is unset or empty, `parseConfig` returns `grokApiKey: null` (via `process.env.GROK_API_KEY || null`, so `GROK_API_KEY=` is treated the same as unset; no throw) and `index.ts` derives `grokEnabled = config.grokApiKey !== null`. When `grokEnabled === false`: + +- Startup logs: `"No GROK_API_KEY provided, disabling Grok support"`. +- **`config.grokContactId` is still restored from the state file** (the lookup runs unconditionally before the `if (grokEnabled)` block). This ensures `getGroupComposition` can identify Grok members so the one-way gate can remove them when a team member sends a text message — even while Grok API is disabled. Without this, Grok members would become "phantom" members: physically present in groups but invisible to the state machine, preventing the gate from firing and causing dual responses (Grok + team) if Grok is later re-enabled. +- The Grok profile is not resolved or created (no `apiListUsers`/`apiCreateActiveUser` for "Grok"; no invite link issued). +- `GrokApiClient` is not instantiated. +- `SupportBot` receives `grokApi = null` and `grokUserId = null`. +- Bot command list registered at startup contains only `/team` — `/grok` is not advertised. +- Grok event handlers (`receivedGroupInvitation`, `connectedToGroupMember`, Grok-side `newChatItems`) are not registered. Handlers that are shared with the main profile (e.g. `onMemberConnected`) remain correct because their Grok checks are guarded by `this.config.grokContactId !== null`. +- Customer-facing messages (`queueMessage`, `noTeamMembersMessage`) accept a `grokEnabled` flag and drop the `/grok` clause when false. +- If the customer still types `/grok` manually, `processMainChatItem` rewrites `cmd` to `null` when `rawCmd?.keyword === "grok" && !this.grokEnabled`, so the dispatcher treats it as an unrecognized command (same as any other plain text). +- Defense in depth: `activateGrok` and `processGrokChatItem` short-circuit on entry when `this.grokApi === null`; `withGrokProfile` throws if called with `grokUserId === null`. + +Type signatures affected: +- `Config.grokApiKey: string | null` +- `SupportBot` constructor: `chat, grokApi: GrokApiClient | null, config, mainUserId, grokUserId: number | null, desiredCommands: T.ChatBotCommand[]` — `desiredCommands` is required (used by `sendToGroup`'s lazy per-group commands sync; see §20.4 suite 30 and the §7 "Note" describing `syncGroupCommands`). +- `queueMessage(timezone: string, grokEnabled: boolean): string` +- `noTeamMembersMessage(grokEnabled: boolean): string` (was a plain `const string`) + +### Grok join flow + +**Critical:** `activateGrok` awaits `waitForGrokJoin(120s)` which depends on future events dispatched through the same sequential event loop (`runEventsLoop` in api.ts). Awaiting it in an event handler deadlocks — the event loop is blocked waiting for events it can't dispatch. **Solution:** All `activateGrok` calls use `fireAndForget()` — tracked but not awaited. Tests call `bot.flush()` to await completion. + +**Main profile side (invite + failure detection):** +0. Send `grokInvitingMessage` ("Inviting Grok, please wait...") +1. **Set `grokInitialResponsePending.add(groupId)` FIRST** — the gate must be raised before any operation that could make Grok recognizable to `onGrokNewChatItems`. Specifically: before `apiAddMember`, before `pendingGrokJoins` is set, and before `bufferedGrokInvitations` is drained (which populates `reverseGrokMap`). Without this ordering, the sequence `apiAddMember → pendingGrokJoins.set → drain → reverseGrokMap.set → gate.add` contains a window where `reverseGrokMap` identifies the group as a Grok-active group but the gate is still DOWN. A customer message arriving in that window triggers a per-message response concurrent with the initial combined response — producing duplicate Grok replies. Every error path below MUST clear the gate. +2. **Pre-check via `apiListMembers`**: silent return if Grok is already in the group in any non-terminal status (covers `GSMemInvited`, which the SimpleX API would otherwise resend the invitation for without throwing). Then `apiAddMember(groupId, grokContactId, Member)` → get `member.memberId`. On `groupDuplicateMember` (race between pre-check and add — Grok joined as Connected meanwhile), **clear the gate** and silent return — the in-flight activation handles the outcome. On any other error, clear the gate, revert state, send `grokUnavailableMessage`. +3. Store `pendingGrokJoins.set(memberId, mainGroupId)` +4. Drain `bufferedGrokInvitations` — if the `receivedGroupInvitation` event arrived during step 2's await (race condition), process it now. (The gate is already up from step 1, so `onGrokNewChatItems` suppresses any per-message responses during drain and the subsequent join.) +5. `waitForGrokJoin(120s)` — awaits resolver from Grok profile's `connectedToGroupMember` (step 8 below) +6. Timeout → notify customer (`grokUnavailableMessage`), send queue message if was WELCOME→GROK, fall back to QUEUE (CAS-guarded: only if `customData.state` is still `GROK` — a concurrent `/team` that switched to `TEAM-PENDING` is respected), clear `grokInitialResponsePending` + +**Grok profile side (independent, triggered by its own events):** +7. `receivedGroupInvitation` → look up `pendingGrokJoins` by `evt.groupInfo.membership.memberId`. If found, auto-accept via `apiJoinGroup(groupId)`, set up `grokGroupMap` and `reverseGrokMap`. If not found (race: event arrived before step 2), buffer in `bufferedGrokInvitations` for step 3. Grok is NOT yet connected — cannot read history or send messages. +8. `connectedToGroupMember` → Grok now fully connected. Uses `reverseGrokMap` to find `mainGroupId`, resolves `grokJoinResolvers` — this unblocks step 5. + +**Back in `activateGrok` (after step 5 resolves):** +9. Read visible history — last 100 messages — build Grok API context (customer messages → `user` role) +10. If no customer messages found (visible history disabled or API failed), send generic greeting asking customer to repeat their question +11. Call Grok HTTP API (outside mutex) +12. Send response via `apiSendTextMessage` (through mutex with Grok profile) +13. Clear `grokInitialResponsePending` (via `finally` block — runs on success, failure, or early return). After this, per-message responses from `onGrokNewChatItems` resume normally for subsequent customer messages. Note: because the gate is raised at step 1 (before any other work), the `finally` block MUST be wired to cover every code path from step 1 onward — including the `groupDuplicateMember` silent-return and all revert/timeout branches — otherwise per-message responses stay suppressed indefinitely for the affected group. + +```typescript +const pendingGrokJoins = new Map() // memberId → mainGroupId +const bufferedGrokInvitations = new Map() // memberId → buffered event +const grokGroupMap = new Map() // mainGroupId → grokLocalGroupId +const reverseGrokMap = new Map() // grokLocalGroupId → mainGroupId +const grokJoinResolvers = new Map void>() // mainGroupId → resolve fn +const grokInitialResponsePending = new Set() // mainGroupIds where activateGrok is sending initial response +``` + +### Per-message Grok conversation + +Grok profile's `onGrokNewChatItems` handler: +1. **Batch deduplication:** When multiple customer messages arrive in a single `newChatItems` event (e.g., rapid messages delivered as a batch), collect the last customer message per group. Only the last message triggers a Grok API call — earlier messages are included in the history context via `apiGetChat`. Without this, each message in the batch would trigger a separate API call, and earlier calls would include later messages in their history (already in the group) — producing incoherent responses that reference messages "from the future" and duplicate replies. +2. **Initial response gate:** Skip groups where `grokInitialResponsePending` is set (checked via `reverseGrokMap` to translate Grok's local groupId to mainGroupId). This prevents per-message responses from racing with the initial combined response in `activateGrok`. +3. Only trigger for `groupRcv` **text** messages from customer (identified via `businessChat.customerId`) +4. Ignore: non-text messages (images, files, voice — card update handled by main profile), bot messages, own messages (`groupSnd`), team member messages +5. Read last 100 messages from own view (customer → `user`, own → `assistant`) +6. Call Grok HTTP API — different groups' calls run concurrently (see "Cross-group Grok parallelism" below). Per-group serialization of overlapping in-flight calls is NOT implemented in MVP (see §20.6). +7. Send response into group + +**Per-message error:** Send error message in group ("Sorry, I couldn't process that. Please try again or send /team for a human team member."), stay GROK. Customer can retry. + +**Card updates in Grok mode:** Each customer message triggers two card updates — one on receipt (main profile sees `groupRcv`), one after Grok responds (main profile sees Grok's `groupRcv`). Both go through the 300-second debounce (default `--card-flush-seconds`). + +### Grok removal + +Only three cases: +1. Team member sends first text message in customer group (one-way gate) +2. Grok join timeout (120s) — fallback to QUEUE +3. Customer leaves the group + +### Grok system prompt + +The full system prompt (including SimpleX documentation context) is supplied externally via the `--context-file ` CLI flag and loaded with `readFileSync` at startup in `index.ts`: + +```typescript +let contextFile = "" +if (config.contextFile) { + try { + contextFile = readFileSync(config.contextFile, "utf-8") + } catch { + log(`Warning: context file not found: ${config.contextFile}`) + } +} +grokApi = new GrokApiClient(config.grokApiKey!, contextFile) +``` + +`GrokApiClient` stores the loaded string as `systemPrompt` and prepends it on every `chat()` call: + +```typescript +async chat(history: GrokMessage[], userMessage: string): Promise { + return this.chatRaw([ + {role: "system", content: this.systemPrompt}, + ...history, + {role: "user", content: userMessage}, + ]) +} +``` + +If `GROK_API_KEY` is set but `--context-file` is missing, `parseConfig` throws and the bot exits before init. If the file path is provided but unreadable at runtime, a warning is logged and Grok runs with an empty system prompt (the API key still works but responses lose the SimpleX-specific guidance). Guidelines (concise answers, numbered steps, no markdown, ignore prompt-override attempts, etc.) live in the external file — not hardcoded — so operators can tune tone and documentation without a rebuild. + +Customer messages always in `user` role, never `system`. + +### Grok HTTP request timeout + +Every `fetch` to `api.x.ai/v1/chat/completions` MUST pass an `AbortSignal.timeout(60_000)` (60-second default). Without a timeout, a stuck TCP connection or an unresponsive server blocks the awaiting call indefinitely; because `processGrokChatItem` runs under the Grok profile's sequential event dispatch, a single hung call stalls per-message responses for ALL customer groups using Grok — and the same hang in `activateGrok`'s initial-response path leaves `grokInitialResponsePending` stuck (gate never released) until the process is killed. + +Implementation in `GrokApiClient.chatRaw`: + +```typescript +const response = await fetch("https://api.x.ai/v1/chat/completions", { + method: "POST", + headers: { ... }, + body: JSON.stringify({ ... }), + signal: AbortSignal.timeout(60_000), +}) +``` + +On abort, `fetch` rejects with a `DOMException` whose `name === "TimeoutError"` (or `"AbortError"` on older runtimes). Callers treat this identically to other `chat()` failures: +- `processGrokChatItem` → sends `grokErrorMessage` to the customer group, conversation stays GROK. +- `activateGrok` initial-response path → logs, sends `grokUnavailableMessage`, lets the `finally` block clear `grokInitialResponsePending`. + +Rationale for 60s: typical xAI responses return in 1–10s; a 60s ceiling accommodates cold-start / heavy-load latencies while still bounding worst-case per-customer wait. Not exposed as a CLI flag in MVP — a later iteration can add `--grok-timeout-seconds` if operator tuning is needed. + +### Cross-group Grok parallelism + +`onGrokNewChatItems` MUST dispatch per-group work concurrently. A naïve `for (const ci of lastPerGroup.values()) { await this.processGrokChatItem(ci) }` serializes calls across unrelated customer groups — if xAI takes 3s per call and five customers message in one event batch, customer #5 waits ~15s instead of ~3s. This is pure latency amplification with no ordering benefit (the groups are independent; within-group order is already preserved by batch deduplication picking the last message). + +Implementation: + +```typescript +async onGrokNewChatItems(evt: CEvt.NewChatItems): Promise { + const lastPerGroup = new Map() + for (const ci of evt.chatItems) { + // filter: groupRcv, customer text, not bot/team + // keep last per groupId + } + await Promise.allSettled( + [...lastPerGroup.values()].map((ci) => this.processGrokChatItem(ci)), + ) +} +``` + +Why `Promise.allSettled` (not `Promise.all`): one group's Grok API failure MUST NOT cancel or reject pending work for other groups. Each `processGrokChatItem` already handles its own errors (sends `grokErrorMessage`, logs); the outer handler only needs to wait until all per-group tasks finish before returning control to the event dispatcher. + +Concurrency bound: the number of distinct customer groups that have new Grok-eligible messages in a single event batch — typically ≤ the SimpleX batch-delivery size, practically small. No global semaphore needed in MVP. If xAI rate limits become a concern, add a shared semaphore later; orthogonal to this fix. + +Ordering guarantees preserved: +- Within a group, batch deduplication still picks only the latest message and earlier messages appear in the history context via `apiGetChat`. +- Across groups, there is no ordering requirement — each customer group is an independent conversation. +- The per-group gate (`grokInitialResponsePending`) still serializes against `activateGrok`'s initial response; this is a group-local check unaffected by cross-group parallelism. + +## 11. Team Group Commands + +| Command | Effect | +|---------|--------| +| `/join ` | Join specified customer group | + +**`/join` handling:** +1. Extract `{keyword, params}` from the chat item with `util.ciBotCommand(chatItem)`. The framework already parses the leading `/keyword` and returns the trimmed remainder as `params` — the handler does not run its own regex over the message text. Cards emit `/'join '`; a team-member tap delivers a chat item whose text is `/join `, which `ciBotCommand` returns as `{keyword: "join", params: ""}`. +2. Convert `params` to a number with `const targetGroupId = Number.parseInt(params, 10)`. If `Number.isNaN(targetGroupId) || targetGroupId <= 0`, reply in the team group with `Error: invalid group id "${params}"` and return. No regex, no `split(":")`, no legacy fallback — operators must use the numeric form (which is what the card always emits). +3. Validate target is a business group (has `businessChat` property) — error in team group if not. +4. Add requesting team member to customer group via `addOrFindTeamMember` (which calls `apiAddMember` + immediately `apiSetMembersRole(Owner)`). +5. On connect, `connectedToGroupMember` re-asserts Owner as an idempotent fallback (see §8). + +**Team member promotion:** Promotion happens at two points, both idempotent: +- **At invite time** — immediately after `apiAddMember`, `addOrFindTeamMember` calls `apiSetMembersRole(groupId, [memberId], Owner)`. The call is wrapped in try/catch: if the member is not yet connected and the API rejects, it's silently ignored (the connect-time promotion covers the fallback). SimpleX persists the role on `GSMemInvited` members so the role is active when they accept. This is only called for *newly invited* members — the pre-check in `addOrFindTeamMember` returns early for any member already in the group in a non-terminal status, so an already-invited member is not re-promoted. +- **On connect** — every `connectedToGroupMember` event in a customer group promotes to Owner unless the member is the customer or Grok. Idempotent. + +**DM handshake:** When a team member joins or connects in the team group, the bot sends a DM with the member's contact ID. Four delivery paths, deduplicated via `sentTeamDMs` Set: + +1. **`onJoinedGroupMember`** — fires when ANY member joins the team group via invite link (`joinedGroupMember` event). Calls `sendTeamMemberDM` without a `memberContact`. Since link-joiners typically have no existing DM contact, this creates the contact via `apiCreateMemberContact(groupId, groupMemberId)`, then sends the invitation with message via `apiSendMemberContactInvitation(contactId, msg)`. +2. **`onMemberConnected`** — `sendTeamMemberDM` called with `memberContact` from the event. If not already sent by path 1: + - If `contactId` exists: sends DM via `apiSendTextMessage`. + - If `contactId` is null: uses the same `apiCreateMemberContact` + `apiSendMemberContactInvitation` path as path 1. +3. **`onMemberContactReceivedInv`** — fires when the member initiates a DM first. Sends the contact ID message immediately. If send fails, queues for `contactConnected`/`contactSndReady`. +4. **`onContactConnected` / `onContactSndReady`** — delivers any pending DM queued by paths 1, 2, or 3. + +DM message: +> Added you to be able to invite you to customer chats later, keep this contact. Your contact ID is `N:name` + +## 12. Message Templates + +```typescript +const welcomeMessage = `Hello! This is a *SimpleX team* support bot - not an AI. +Please ask any question about SimpleX Chat.` + +function queueMessage(timezone: string, grokEnabled: boolean): string { + const hours = isWeekend(timezone) ? "48" : "24" + const base = `The team will reply to your message within ${hours} hours.` + if (!grokEnabled) return base + return `${base} + +If your question is about SimpleX, click /grok for an *instant Grok answer*. + +Send /team to switch back.` +} + +const grokActivatedMessage = `*You are chatting with Grok* - use any language.` + +function teamAddedMessage(timezone: string, grokPresent: boolean): string { + const hours = isWeekend(timezone) ? "48" : "24" + const base = `We will reply within ${hours} hours.` + if (!grokPresent) return base + return `${base} +Grok will be answering your questions until then.` +} + +const teamAlreadyInvitedMessage = "A team member has already been invited to this conversation and will reply when available." + +const teamLockedMessage = "You are now in team mode. A team member will reply to your message." + +function noTeamMembersMessage(grokEnabled: boolean): string { + return grokEnabled + ? "No team members are available yet. Please try again later or click /grok." + : "No team members are available yet. Please try again later." +} + +const grokInvitingMessage = "Inviting Grok, please wait..." + +const grokUnavailableMessage = "Grok is temporarily unavailable. Please try again later or send /team for a human team member." + +const grokErrorMessage = "Sorry, I couldn't process that. Please try again or send /team for a human team member." + +const grokNoHistoryMessage = "I just joined but couldn't see your earlier messages. Could you repeat your question?" +``` + +`teamAddedMessage` takes a second `grokPresent` argument — when the customer switches from GROK → TEAM-PENDING (Grok still in the group until the gate triggers), the message appends a second line telling the customer Grok will keep answering until the team replies. Callers detect this by checking the current group composition for a Grok member before sending. + +**Weekend detection:** +```typescript +function isWeekend(timezone: string): boolean { + const day = new Intl.DateTimeFormat("en-US", {timeZone: timezone, weekday: "short"}).format(new Date()) + return day === "Sat" || day === "Sun" +} +``` + +## 13. Direct Message Handling + +If a user contacts the bot via a regular direct-message address (not business address), the bot replies with the business address link and does not continue the conversation. The reply is guarded by `chatItem.content.type === "rcvMsgContent"` — only actual text messages trigger the business address reply. System events on the DM contact (e.g. `contactConnected`, `rcvDirectEvent`) are ignored to prevent spam. + +## 14. Persistent State + +**State file:** `{dbPrefix}_state.json` — three keys: + +| Key | Type | Why persisted | +|-----|------|---------------| +| `teamGroupId` | number | Team group created once on first run | +| `grokContactId` | number | Bot↔Grok contact takes 60s to establish | +| `grokUserId` | number | Identifies the Grok user by ID across restarts; prevents silent mis-matching if the Grok profile is ever renamed | + +**Not persisted:** + +| State | Where it lives | +|-------|---------------| +| `state`, `cardItemId`, `complete` | Customer group's `customData` | +| `mainUserId` | Returned by `bot.run()` on startup; created fresh per DB | +| Message counts, timestamps | Derived from chat history | +| Customer name | Group display name | +| `pendingGrokJoins` | In-flight during 120s window only | +| `grokInitialResponsePending` | In-flight during `activateGrok` initial response only | +| Owner promotion | Idempotent: fired at invite time in `addOrFindTeamMember` and again on every `memberConnected` | + +**Failure modes:** +- State file deleted → new team group created, Grok contact re-established (60s delay) +- Grok remains in groups it was already in — self-contained, continues responding via own events + +## 15. Error Handling + +| Scenario | Handling | +|----------|----------| +| ChatApi init fails | Exit (let process manager restart) | +| Active user is Grok on restart | Pre-init DB, find main user, set active, close — before `bot.run()` | +| Grok join timeout (120s) | Notify customer, fall back to QUEUE | +| Grok API error (initial or per-message) | Send error in group, stay GROK. Customer can retry or `/team`. | +| `apiAddMember` fails | Send error msg, stay in current state | +| `groupDuplicateMember` on Grok invite | Silent return — in-flight activation handles the outcome (customer sent `/grok` again before join completed) | +| `apiRemoveMembers` fails | Ignore (member may have left) | +| `apiDeleteChatItems` fails (card) | Ignore, post new card, overwrite `customData` | +| Customer leaves | Cleanup in-memory state, card remains | +| Team member leaves (no message sent) | State stays `TEAM-PENDING` (`customData.state` persists). Customer's next `/team` re-adds silently. | +| Team member leaves (message sent) | State stays `TEAM` (`customData.state` persists). Customer's next `/team` re-adds silently. | +| No `--auto-add-team-members` (`-a`) configured | `/team` → "no team members available yet" | +| `grokContactId` unavailable | `/grok` → "temporarily unavailable" | +| Member already in group when `/team` re-runs | `addOrFindTeamMember` pre-checks via `apiListMembers` and skips BOTH `apiAddMember` and the invite-time `apiSetMembersRole(Owner)` entirely if the contact is present in any non-terminal status (so an `Invited`-but-not-yet-accepted member is never re-invited — the SimpleX API would otherwise resend the invitation for `GSMemInvited` — and is never re-promoted) | + +## 16. API Call Map + +| # | Operation | Instance | Method | When | +|---|-----------|----------|--------|------| +| 1 | Init bot | main | `bot.run()` | Startup | +| 2 | List users | chat | `apiListUsers()` | Startup — resolve profiles | +| 3 | Create Grok user | chat | `apiCreateActiveUser()` | First run | +| 4 | Set active user | chat | `apiSetActiveUser(userId)` | Before every API call (via mutex) | +| 5 | Resolve team group | main | `apiNewGroup()` / state file | Startup | +| 6 | Create team invite link | main | `apiCreateGroupLink()` | Startup | +| 7 | Delete team invite link | main | `apiDeleteGroupLink()` | 10min / shutdown | +| 8 | Auto-accept DM | main | `apiSetAutoAcceptMemberContacts(userId, true)` | Startup | +| 9 | List contacts | main | `apiListContacts()` | Startup — validate members | +| 10 | Establish Grok contact | main+grok | `apiCreateLink()` + `apiConnectActiveUser()` | First run | +| 11 | Update group profile | main | `apiUpdateGroupProfile()` | Business request; startup (conditional — only if preferences differ) | +| 12 | Send msg to customer | main | `apiSendTextMessage([Group, gId], text)` | Various | +| 13 | Post card to team group | main | `apiSendMessages(chatRef, [{card text with /'join ' final line}])` | Card create/update — one message per card | +| 14 | Delete card | main | `apiDeleteChatItems([Group, teamGId], [cardItemId], "broadcast")` | Card update | +| 15 | Set customData | main | `apiSetGroupCustomData(gId, data)` | Card lifecycle | +| 16 | Invite Grok | main | `apiAddMember(gId, grokContactId, Member)` | `/grok` | +| 17 | Grok joins | grok | `apiJoinGroup(gId)` | `receivedGroupInvitation` | +| 18 | Grok reads history | grok | `apiGetChat([Group, gId], 100)` | After join + per message | +| 19 | Grok sends response | grok | `apiSendTextMessage([Group, gId], text)` | After API call | +| 20 | Add team member | main | `apiAddMember(gId, teamContactId, Member)` | `/team`, `/join` — only when not already in group | +| 21 | Promote to Owner | main | `apiSetMembersRole(gId, [memberId], Owner)` | Immediately after #20 (invite-time) AND `connectedToGroupMember` (fallback) | +| 22 | Remove Grok | main | `apiRemoveMembers(gId, [memberId])` | Gate trigger / timeout / leave | +| 23 | List members | main | `apiListMembers(gId)` | State derivation, duplicate check | +| 24 | Register team commands | main | `apiUpdateGroupProfile(teamGId, profile)` | Startup — register `/join` in team group | +| 25 | Get group info | main | `apiListGroups()` + find by ID | Card compose — read `customData.cardItemId` from `groupInfo` | +| 26 | Create DM contact | main | `apiCreateMemberContact(gId, memberId)` | `joinedGroupMember` / `onMemberConnected` — bot-initiated DM with team member | +| 27 | Send DM invitation | main | `apiSendMemberContactInvitation(contactId, msg)` | After #26 — sends invite with message in one step | + +## 17. Implementation Sequence + +**Phase 1: Scaffold** +- `package.json`, `tsconfig.json`, `config.ts`, `util.ts` (isWeekend, profileMutex) +- `index.ts`: init ChatApi, resolve both profiles, state file, startup sequence +- **Verify:** Instance inits, profiles resolved, Grok contact established, team group created + +**Phase 2: Event processing + cards** +- `bot.ts`: SupportBot class, state derivation helpers, event dispatch +- `cards.ts`: CardManager — format, debounce, lifecycle (create/update/cleanup) +- `messages.ts`: all templates +- Handle `acceptingBusinessRequest` → enable file uploads + visible history +- Handle `newChatItems` → WELCOME/QUEUE routing, card creation +- Handle DM → reply with business address link +- **Verify:** Customer connects → welcome → sends msg → card appears in team group → queue reply + +**Phase 3: Grok integration** +- `grok.ts`: GrokApiClient with system prompt + docs +- Grok event handlers (invitation → join, newChatItems → respond) +- `/grok` activation: invite, wait join, Grok reads history + responds independently +- `/grok` as first message (WELCOME → GROK, skip queue) +- Per-message Grok conversation + serialization per group +- **Verify:** `/grok` → Grok joins as separate participant → responds from "Grok" + +**Phase 4: Team mode + one-way gate** +- `/team` → add team members, Grok stays +- One-way gate: detect first team text → remove Grok, disable `/grok` +- `/join` command in team group (validate business group, add member, promote Owner) +- DM handshake with team members +- Team member promotion on `connectedToGroupMember` +- **Verify:** Full flow: QUEUE → /grok → GROK → /team → TEAM-PENDING → team msg → TEAM + +**Phase 5: Polish** +- Edge cases: customer leave, Grok timeout, member leave, restart recovery +- Team group invite link lifecycle +- Graceful shutdown +- Supply Grok context via `--context-file ` at runtime (required when `GROK_API_KEY` is set) +- End-to-end test all flows + +## 18. Self-Review Requirement + +Each code artifact must undergo adversarial self-review/fix loop: +1. Write/edit code +2. Self-review against this plan: correctness, completeness, all state transitions, all API calls, all error cases +3. Fix issues found +4. Repeat until **2 consecutive zero-issue passes** +5. Report completion → user reviews → if changes needed, restart from step 1 + +## 19. Verification + +**Startup:** +```bash +cd apps/simplex-support-bot +npm install +# With Grok support: +GROK_API_KEY=xai-... npx ts-node src/index.ts \ + --team-group SupportTeam \ + --timezone America/New_York \ + --context-file ./context.md + +# Without Grok (logs "No GROK_API_KEY provided, disabling Grok support"): +npx ts-node src/index.ts \ + --team-group SupportTeam \ + --timezone America/New_York +``` + +**Test scenarios:** +1. Connect → verify welcome message, business address link printed to stdout +2. Send question → verify card appears in team group (🆕), queue reply received +3. `/grok` → verify Grok joins, responses from "Grok", card updates to 🤖 +4. `/grok` as first message → verify WELCOME→GROK, no queue message, card 🤖 +5. `/team` in GROK → verify team added, Grok stays, card 👋 Team-pending +6. `/grok` in TEAM-PENDING → verify Grok still responds +7. Team member sends text → verify Grok removed, `/grok` rejected, card → 💬 +8. `/grok` in TEAM → verify "team mode" rejection +9. `/team` when already invited → verify "already invited" message +10. Card debouncing: multiple rapid events → verify single card update per 300s flush (default) +11. `/join` from team group → verify team member added to customer group, promoted to Owner +12. `/join` with non-business group → verify error +13. Weekend → verify "48 hours" +14. Customer leaves → verify cleanup, card remains +15. Grok timeout → verify fallback to QUEUE, queue message sent +16. Grok API error (per-message) → verify error in group, stays GROK +17. Grok no-history fallback → verify generic greeting sent +18. Non-text message in GROK mode → verify no Grok API call, card updated +19. Team/Grok reaction → verify card auto-complete (✅ icon, "done") +20. DM contact text message → verify business address link reply +21. DM contact non-message event (e.g. contactConnected) → verify no reply (rcvMsgContent guard) +22. DM handshake via `joinedGroupMember` → team member joins team group via link → verify `apiCreateMemberContact` + `apiSendMemberContactInvitation` called, contact ID message sent +23. DM handshake via `connectedToGroupMember` → verify contact ID message sent (dedup with #22) +24. Restart → verify same team group + Grok contact from state file, cards resume via `customData` +25. No `--auto-add-team-members` (`-a`) → `/team` → verify "no team members available" +26. Repeated `/team` while members are still in `Invited` status → verify `apiAddMember` is NOT called again (pre-check in `addOrFindTeamMember` returns the existing member) +27. Team member leaves (no message sent) → verify revert to QUEUE +28. Team member leaves (message sent), customer sends `/team` → verify re-adds team members +29. Card preview sender prefixes → verify first message in each consecutive sender run gets `Name:` prefix, subsequent same-sender messages do not +30. `/team` after all team members left → verify re-adds team members (not "already invited") + +### Critical Reference Files + +- **Native library API:** `packages/simplex-chat-nodejs/src/api.ts` +- **Bot automation:** `packages/simplex-chat-nodejs/src/bot.ts` +- **Utilities:** `packages/simplex-chat-nodejs/src/util.ts` +- **Types:** `packages/simplex-chat-client/types/typescript/src/types.ts` +- **Events:** `packages/simplex-chat-client/types/typescript/src/events.ts` +- **Product spec:** `apps/simplex-support-bot/plans/20260207-support-bot.md` + +## 20. Testing + +Vitest 1.x (Node 18 compatible). All tests verify **observable behavior** — messages sent, members added/removed, cards posted/deleted, API calls made — never internal state. + +### 20.1 Mock Infrastructure + +**Approach:** Vite resolve aliases redirect native-dependent packages to lightweight JS stubs at build time. Tests import from TypeScript source (`./src/bot.js`) — Vitest transpiles inline, so mocks apply before any code runs. + +**Files:** + +| File | Purpose | +|------|---------| +| `bot.test.ts` | All tests (co-located with source) | +| `vitest.config.ts` | Resolve aliases, globals, timeout | +| `test/__mocks__/simplex-chat.js` | CJS stub: `api.ChatApi`, `util.ciContentText`, `util.ciBotCommand`, `util.contactAddressStr` | +| `test/__mocks__/simplex-chat-types.js` | CJS stub: `T.ChatType`, `T.GroupMemberRole`, `T.GroupMemberStatus`, `T.GroupFeatureEnabled`, `T.CIDeleteMode` | + +```typescript +// vitest.config.ts +export default defineConfig({ + test: { globals: true, testTimeout: 10000 }, + resolve: { + alias: { + "simplex-chat": path.resolve(__dirname, "test/__mocks__/simplex-chat.js"), + "@simplex-chat/types": path.resolve(__dirname, "test/__mocks__/simplex-chat-types.js"), + }, + }, +}) +``` + +**`MockChatApi`** — inline class in `bot.test.ts`: + +- **Tracking arrays:** `sent`, `added`, `removed`, `joined`, `deleted`, `customData`, `roleChanges`, `profileUpdates`, `memberContacts`, `memberContactInvitations` +- **Simulated DB:** `members` (Map), `chatItems` (Map), `groups` (Map), `activeUserId` +- **Failure injection:** `apiAddMemberWillFail(err?)`, `apiDeleteChatItemsWillFail()` +- **Query helpers:** `sentTo(groupId)`, `lastSentTo(groupId)`, `sentDirect(contactId)` +- `apiSendTextMessage` returns `[{chatItem: {meta: {itemId: N}}}]` — auto-incrementing IDs +- `apiGetChat` returns from `chatItems` map with `chatInfo.groupInfo` from `groups` map +- `apiCreateMemberContact(groupId, groupMemberId)` — returns a contact object with auto-incrementing `contactId`. Tracks calls in `memberContacts` array. +- `apiSendMemberContactInvitation(contactId, msg)` — returns a contact object. Tracks calls in `memberContactInvitations` array. + +**`MockGrokApi`** — inline class: + +- `calls` array tracks `{history, message}` for each `chat()` call +- `willRespond(text)` / `willFail()` control responses +- Resets to default response `"Grok answer"` after each failure + +**Key design:** no `vi.mock()` hoisting — resolve aliases intercept all `require()`/`import()` before module evaluation. Console output silenced via `vi.spyOn(console, "log/error")`. + +### 20.2 Factory Helpers & Event Builders + +Tests construct events via composable helpers: + +```typescript +// Factory helpers +makeConfig(overrides?) // Config with defaults (team group, 2 team members, UTC) +makeGroupInfo(groupId, opts?) // GroupInfo with businessChat, customerId, etc. +makeUser(userId) // {userId, profile: {displayName}} +makeChatItem(opts) // ChatItem with dir/text/memberId/msgType +makeAChatItem(chatItem, groupId?) // AChatItem wrapping chatItem + groupInfo + +// Member factories — typed member objects +makeTeamMember(contactId, name?, groupMemberId?) // team member with standard memberId pattern +makeGrokMember(groupMemberId?) // Grok member (default groupMemberId=7777) +makeCustomerMember(status?) // customer member + +// Event builders — return full newChatItems events +customerMessage(text, groupId?) // from customer in customer group +customerNonTextMessage(groupId?) // non-text (image) from customer +teamMemberMessage(text, contactId?, groupId?) // from team member +grokResponseMessage(text, groupId?) // from Grok in customer group +directMessage(text, contactId) // from direct contact +teamGroupMessage(text, senderContactId?) // in team group +grokViewCustomerMessage(text, msgType?) // customer msg arriving in Grok's view + +// Event factories — return full lifecycle events +connectedEvent(groupId, member, memberContact?) // connectedToGroupMember +leftEvent(groupId, member) // leftMember (auto-sets Left status) +updatedEvent(groupId, chatItem, userId?) // chatItemUpdated +reactionEvent(groupId, added) // chatItemReaction +joinedEvent(groupId, member, userId?) // joinedGroupMember + +// History builders — add to mock chatItems map +addBotMessage(text, groupId?) +addCustomerMessageToHistory(text, groupId?) +addTeamMemberMessageToHistory(text, contactId?, groupId?) +addGrokMessageToHistory(text, groupId?) + +// Assertion helpers — intention-revealing, with debuggable failure messages +expectSentToGroup(groupId, substring) // message containing substring sent to group +expectNotSentToGroup(groupId, substring) // no message containing substring sent to group +expectDmSent(contactId, substring) // DM containing substring sent to contact +expectAnySent(substring) // any message (group or DM) containing substring +expectMemberAdded(groupId, contactId) // apiAddMember called with groupId + contactId +expectCardDeleted(cardItemId) // apiDeleteChatItems called with cardItemId +expectMemberContactCreated(groupId, memberId) // apiCreateMemberContact called +expectMemberContactInvSent(contactId) // apiSendMemberContactInvitation called +``` + +### 20.3 State Setup Helpers + +Each helper reaches a specific state, composing from simpler helpers: + +```typescript +async function reachQueue(groupId?) // send first msg → QUEUE (adds queue msg to history) +async function reachGrok(groupId?) // reachQueue → /grok → simulateGrokJoinSuccess → GROK +async function reachTeamPending(groupId?) // reachQueue → /team → TEAM-PENDING +async function reachTeam(groupId?) // reachTeamPending → add team member to mock → team msg → TEAM +``` + +**`simulateGrokJoinSuccess(mainGroupId?)`** — simulates the async Grok join flow: +1. Waits 10ms (lets `activateGrok` reach `waitForGrokJoin`) +2. Fires `onGrokGroupInvitation` (Grok accepts invite) +3. Fires `onGrokMemberConnected` (Grok fully connected → resolver called) + +Called as: `const p = simulateGrokJoinSuccess(); await bot.onNewChatItems(...); await p;` + +### 20.4 Test Catalog (154 tests, 31 suites) + +#### 1. Welcome & First Message (4 tests) +- first message → queue reply + card created with /join command +- non-text first message → no queue reply, no card +- second message → no duplicate queue reply +- unrecognized /command → treated as normal message (triggers queue) + +#### 2. /grok Activation (5 tests) +- /grok from QUEUE → Grok invited, grokActivatedMessage sent (after join confirms) +- /grok as first message → WELCOME→GROK, no queue message, card created +- /grok in TEAM → rejected with teamLockedMessage +- /grok when grokContactId is null → grokUnavailableMessage +- /grok as first message + Grok join fails → queue message sent as fallback + +#### 3. Grok Conversation (11 tests) +- Grok per-message: reads history, calls API, sends response +- customer non-text → no Grok API call +- Grok API error → grokErrorMessage sent +- Grok ignores bot commands from customer +- Grok ignores non-customer messages +- Grok ignores own messages (groupSnd) +- batch: multiple customer messages in one event → only last triggers Grok API call +- batch: messages from different groups → each group gets one response +- batch: non-customer messages filtered, only customer messages trigger response +- batch: across groups → Grok calls overlap in-flight (parallel `Promise.allSettled` dispatch, proven via gated `MockGrokApi.chat`) + +#### 4. /team Activation (4 tests) +- /team from QUEUE → ALL team members added, teamAddedMessage sent +- /team as first message → WELCOME→TEAM-PENDING, no queue message +- /team when already activated (members present) → teamAlreadyInvitedMessage +- /team with no team members → noTeamMembersMessage + +#### 5. One-Way Gate (5 tests) +- team member first TEXT → Grok removed if present +- team member empty text → Grok NOT removed +- /grok after gate → teamLockedMessage +- customer text in TEAM → no bot reply, card update scheduled +- /grok in TEAM-PENDING → invite Grok if not present + +#### 5b. One-Way Gate with Grok Disabled (2 tests) +- team text removes Grok even when grokApi is null +- Grok does not respond when disabled even if grokContactId is set + +#### 6. Team Member Lifecycle (6 tests) +- team member connected → promoted to Owner +- customer connected → NOT promoted +- Grok connected → NOT promoted +- all team members leave → reverts to QUEUE +- /team after all members left (TEAM-PENDING, no msg sent) → re-adds members +- /team after all members left (TEAM, msg was sent) → re-adds members + +#### 7. Card Dashboard (7 tests) +- first message creates card with customer name + /join +- card final line is `/'join '` (single-quoted, numeric id only, no `:name` suffix) +- card update deletes old, posts new +- apiDeleteChatItems failure → ignored, new card posted +- customData stores cardItemId through flush cycle +- concurrent `mergeCustomData` on same group → both patches survive (per-group `customDataMutex` serializes read-modify-write; without the mutex the second write clobbers the first) +- customer leaves → customData cleared + +#### 8. Card Debouncing (5 tests) +- rapid schedules → single card update on flush +- multiple groups pending → each reposted once +- card create is immediate (not debounced) +- flush with no pending → no-op +- flush on group with no `cardItemId` → `createCard` posts a new card (proves `flushOne` dispatches to create-path so a failed `createCard` retries) + +#### 9. Card Format & State Derivation (6 tests) +- QUEUE state derived (no Grok/team) +- WELCOME state derived (customData has no cardItemId) +- GROK state derived (Grok member present) +- TEAM-PENDING derived (team present, no team message) +- TEAM derived (team present + message sent) +- message count excludes bot's own + +#### 10. /join Command (6 tests) +- /join (the only accepted form) → team member added; `params` from `ciBotCommand` is parsed via `Number.parseInt`, no regex +- /join : (historic suffix) → still parses because `Number.parseInt(":", 10)` stops at the colon — handler does not strip the suffix deliberately; the suffix is never emitted by the card +- /join with non-numeric `params` (e.g. `/join abc`) → error reply in team group, no `apiAddMember` call +- /join non-business group → error +- /join non-existent groupId → error +- customer /join in customer group → treated as normal message + +#### 11. DM Handshake (6 tests) +- team member joins team group → DM with contact ID +- name with spaces → single-quoted +- pending DM delivered on contactConnected +- team member with no DM contact → creates member contact via `apiCreateMemberContact` and sends invitation via `apiSendMemberContactInvitation` +- joinedGroupMember in team group → creates member contact via `apiCreateMemberContact` and sends invitation via `apiSendMemberContactInvitation` +- no duplicate DM when sendTeamMemberDM succeeds AND onMemberContactReceivedInv fires + +#### 12. Direct Messages (3 tests) +- regular DM → business address link reply +- DM without business address → no reply +- non-message DM event (e.g. contactConnected) → no reply (rcvMsgContent guard) + +#### 13. Business Request (1 test) +- acceptingBusinessRequest → enables file uploads + visible history + +#### 14. chatItemUpdated Handler (3 tests) +- business group → card update scheduled +- non-business group → ignored +- wrong user → ignored + +#### 15. Reactions (2 tests) +- reaction added → card update scheduled +- reaction removed → no card update + +#### 16. Customer Leave (4 tests) +- customer leaves → customData cleared +- Grok leaves → maps cleaned, no crash +- team member leaves → logged, no crash +- leftMember in non-business group → ignored + +#### 17. Error Handling (3 tests) +- apiAddMember fails (Grok) → grokUnavailableMessage +- /grok while Grok already present (any non-terminal status, including `Invited`) → pre-check silent-returns, no `apiAddMember` call. Plus race coverage: simulated `groupDuplicateMember` thrown by `apiAddMember` → silent return, no further state change +- /team while team member already present (any non-terminal status, including `Invited`) → `apiAddMember` not called for that member + +#### 18. Profile / Event Filtering (4 tests) +- newChatItems from Grok profile → ignored by main handler +- Grok events from main profile → ignored by Grok handlers +- own messages (groupSnd) → ignored +- non-business group messages → ignored + +#### 19. Grok Join Flow (6 tests) +- receivedGroupInvitation → apiJoinGroup called (full async flow) +- unmatched Grok invitation → buffered (not joined until activateGrok drains) +- buffered invitation drained after pendingGrokJoins set → apiJoinGroup called +- per-message responses suppressed during activateGrok initial response (grokInitialResponsePending gate) +- per-message responses resume after activateGrok completes +- activateGrok `groupDuplicateMember` path → gate cleared by outer `finally` (subsequent per-message event still triggers Grok; proves the outer `try/finally` covers every exit path from the entry-time `gate.add`, not just the initial-response section) + +#### 20. Grok No-History Fallback (1 test) +- Grok joins but sees no customer messages → grokNoHistoryMessage + +#### 21. Non-customer card updates (2 tests) +- Grok response → card update scheduled +- team member message → card update scheduled + +#### 22. End-to-End Flows (3 tests) +- WELCOME → QUEUE → /team → TEAM-PENDING → team msg → TEAM +- WELCOME → /grok first msg → GROK +- multiple concurrent conversations are independent + +#### 23. Message Templates (5 tests) +- welcomeMessage includes/omits group links +- grokActivatedMessage content +- teamLockedMessage content +- queueMessage mentions hours + +#### 24. State persistence in customData (5 tests) +- `deriveState` returns `WELCOME` when `customData.state` is absent +- first customer non-command message → handler writes `customData.state = "QUEUE"` +- `/grok` handler → writes `customData.state = "GROK"` +- `/team` handler → writes `customData.state = "TEAM-PENDING"` immediately (before team member accepts) +- first team-member text message → gate writes `customData.state = "TEAM"`; state persists when team member subsequently leaves (not demoted to `QUEUE`) + +#### 25. Card Preview Sender Prefixes (14 tests) +- single customer message → name prefix +- consecutive same-sender → prefix only on first +- alternating senders → each run gets prefix +- Grok messages → "Grok:" prefix +- team member messages → display name prefix +- bot messages (groupSnd) → excluded +- non-text content → media label ([image], [voice], etc.) +- empty messages → skipped +- truncation at maxTotal and maxPer limits (newest messages kept, oldest truncated) +- customer identified by memberId (not contactId) +- newlines in message text → replaced with spaces +- newlines in customer display name → sanitized in card header (card header is the only place the display name appears; `/join` is numeric id only) + +#### 26. Restart Card Recovery (10 tests) +- refreshAllCards refreshes groups with active cards +- no active cards → no-op +- ignores groups without cardItemId in customData +- orders by cardItemId ascending (oldest first, newest last) +- skips cards marked complete +- deletes old card before reposting +- ignores delete failure (>24h old card) +- card flush writes complete: true for auto-completed conversations +- card flush clears complete flag when conversation becomes active again +- continues on individual card failure + +#### 27. joinedGroupMember Event Filtering (2 tests) +- joinedGroupMember in non-team group → ignored +- joinedGroupMember from wrong user → ignored + +#### 28. parseConfig Validation (6 tests) +- `--complete-hours` non-numeric → throws with message including the flag name and raw value +- `--complete-hours` negative → throws +- `--card-flush-seconds` non-numeric → throws +- `--timezone` invalid IANA → throws (probe `Intl.DateTimeFormat` at parse time) +- `--complete-hours 0` → accepted (disables auto-complete) +- valid IANA timezone → accepted + +#### 29. GrokApiClient HTTP timeout (1 test) +- `chat()` calls `AbortSignal.timeout(60_000)` and passes the signal to `fetch` (spies on `AbortSignal.timeout` and on `globalThis.fetch`; proves the timeout is wired through without waiting 60s of wall-clock) + +#### 30. Command sync in sendToGroup (5 tests) +Covers the lazy per-group commands sync introduced with `updateProfile: false`. `sendToGroup` unconditionally calls `syncGroupCommands(groupId)` before dispatching. That helper reads the group via `apiGetChat` (local-only) and issues `apiUpdateGroupProfile` with the merged `groupPreferences.commands` only if the current list doesn't match `desiredCommands`. Groups are cached in `syncedGroups: Set` per process, so later sends skip the read entirely. +- first send → one `apiUpdateGroupProfile` call with `groupPreferences.commands = desiredCommands`; existing `groupProfile.displayName` / `fullName` preserved in the payload; message still delivered (text content is irrelevant — sync always runs) +- group already has desired commands in DB → no `apiUpdateGroupProfile` call, but `syncedGroups` is still populated (next send with different DB state still skips — cache honored) +- cache: two sends to same group → sync fires only once; both messages delivered +- different groups → each synced independently +- existing `groupPreferences` fields (e.g. `files`, `reactions`) are preserved in the update payload; only `commands` changes + +### 20.5 Conventions + +- **File:** `bot.test.ts` (co-located with source, imports from `./src/*.js`) +- **Framework:** Vitest 1.x (Node 18 compatible) with `describe`/`test`/`beforeEach` +- **Mocking:** Vite resolve aliases (not `vi.mock`) — prevents native addon loading +- **Titles:** plain English, `→` separates action from outcome +- **Assertions:** verify observable effects only — messages, API calls, card content +- **No internal state assertions** — never peek at private fields +- **Each test is self-contained** — `beforeEach(() => setup())` creates fresh mocks +- **State helpers compose** — `reachTeam()` calls `reachTeamPending()` which calls `reachQueue()` +- **Grok join simulation** — `simulateGrokJoinSuccess()` uses 10ms setTimeout to fire events during `waitForGrokJoin` await. Tests call `await bot.flush()` after simulation to await fire-and-forget `activateGrok` completion. +- **No fake timers** — real timers everywhere; flush called explicitly via `cards.flush()` and `bot.flush()`. Suite 29 spies on `AbortSignal.timeout` rather than advancing a fake clock so it does not need fake timers either. + +### 20.6 Test Coverage Notes + +**Covered vs plan catalog:** +- §20.4 suites 1-13, 15, 17-30 plus 5b fully covered (154 tests across 31 suites) +- Weekend detection (`util.isWeekend`) — not unit-tested; depends on `Intl.DateTimeFormat(new Date())`, would need clock mocking. Not present in the §20.4 catalog. +- Profile Mutex serialization — not a standalone suite in §20.4; verified implicitly through all other tests (MockChatApi tracks activeUserId). +- Startup & state persistence (`index.ts` path) — not unit-tested; requires native ChatApi. Integration-test only. Includes `deleteInviteLink` (profileMutex + `apiSetActiveUser` before `apiDeleteGroupLink`), the conditional `apiUpdateGroupProfile` (compare `fullGroupPreferences` before calling), the best-effort `apiCreateGroupLink` (catch + log on SMP relay failure), the predicate-filtered `chat.wait("contactConnected", ...)` used to identify the Grok contact (§4), and the team-group `/join` command registration with `params: "groupId"` (§11). Not in §20.4 catalog. + +**Known plan items NOT implemented (conscious gaps, not test gaps):** +- Per-group Grok API call serialization (plan §10) — not implemented or tested +- Team member replacement on leave after sending — out of MVP scope. No plan section currently asserts it as a requirement; if added later, specify in SPEC §4.2 "Team replies" and implementation plan §15 "Error Handling" together. diff --git a/apps/simplex-support-bot/plans/20260207-support-bot.md b/apps/simplex-support-bot/plans/20260207-support-bot.md new file mode 100644 index 0000000000..6ba1380e07 --- /dev/null +++ b/apps/simplex-support-bot/plans/20260207-support-bot.md @@ -0,0 +1,513 @@ +# SimpleX Support Bot — Product Specification + +## Table of Contents + +1. [What](#1-what) +2. [Why](#2-why) +3. [Principles](#3-principles) +4. [Flows](#4-flows) + - [User flow](#41-user-flow) + - [Team flow](#42-team-flow) +5. [Architecture](#5-architecture) + - [CLI overview](#51-cli-overview) + - [Bot architecture](#52-bot-architecture) + - [Grok integration](#53-grok-integration) + - [Persistent state](#54-persistent-state) + +--- + +## 1. What + +A support bot for SimpleX Chat. Customers connect via a business address and get a private group where they can ask questions. The bot triages inquiries through AI (Grok) or human team members. The team sees all active conversations as cards in a single dashboard group. + +## 2. Why + +- **Instant answers.** Grok handles common questions about SimpleX Chat without team involvement. +- **Organized routing.** Every customer conversation appears as a card in the team group — the team sees everything in one place without joining individual conversations. +- **No external tooling.** Everything runs inside SimpleX Chat. No ticketing system, no separate dashboard. +- **Privacy.** Customers talk to the bot in private groups. Only the team sees the messages. + +--- + +## 3. Principles + +- **Opt-in**: Grok is never used unless the user explicitly chooses it. +- **User in control**: The user can switch to Grok or team before a team member replies. Once a team member sends a message, the conversation stays with the team. The user always knows who they are talking to. +- **Minimal friction**: No upfront choices or setup — the user just sends their question. +- **Ultimate transparency**: The user always knows whether they are talking to a bot, Grok, or a human, and what happens with their messages. + +--- + +## 4. Flows + +### 4.1 User Flow + +#### Step 1 — Welcome (on connect, no choices, no friction) + +When a user scans the support bot's QR code or clicks its address link, SimpleX creates a **business group** — a special group type where the customer is a fixed member identified by a stable `customerId`, and the bot is the host. The bot auto-accepts the connection and enables file uploads and visible history on the group. + +If a user contacts the bot via a regular direct-message address instead of the business address, the bot replies with the business address link and does not continue the conversation. Only actual text messages trigger this reply — system events (e.g. `contactConnected`) on the DM contact are ignored. + +Bot sends the welcome message automatically as part of the connection handshake — not triggered by a message: +> Hello! This is a *SimpleX team* support bot - not an AI. +> Please ask any question about SimpleX Chat. + +#### Step 2 — After user sends first message + +The bot's "first message" detection works by inspecting the group's `customData`. Until the bot has produced its first response (and written `cardItemId` to `customData`), the group is in the welcome state. + +On the customer's first message the bot does two things: +1. Creates a card in the team group (🆕 icon, with `/join` command) +2. Sends the queue message to the customer: + +> The team will reply to your message within 24 hours. +> +> If your question is about SimpleX, click /grok for an *instant Grok answer*. +> +> Send /team to switch back. + +On weekends, the bot says "48 hours" instead of "24 hours". + +When the bot is started without `GROK_API_KEY`, the `/grok` paragraphs are omitted — the customer only sees the first line about the team reply window. + +Each subsequent message updates the card — icon, wait time, message preview. The team reads the full conversation by joining via the card's `/join` command. + +#### Step 3 — `/grok` (Grok mode) + +Available in WELCOME, QUEUE, or TEAM-PENDING state (before any team member sends a message). If Grok is already being invited (e.g. customer sent `/grok` multiple times before Grok finished joining), the duplicate is silently ignored — the in-flight activation handles the outcome. If `/grok` is the customer's first message, the bot transitions directly from WELCOME → GROK — it creates the card with 🤖 icon and does not send the queue message. Triggers Grok activation (see [5.3 Grok integration](#53-grok-integration)). If Grok fails to join within 120 seconds, the bot notifies the user and the state falls back to QUEUE (the queue message is sent at this point). + +Bot immediately replies: +> Inviting Grok, please wait... + +Once Grok joins and connects: +> *You are chatting with Grok* - use any language. + +Grok is added as a separate participant so the user can differentiate bot messages from Grok messages. + +Grok is prompted as a privacy expert and support assistant who knows SimpleX Chat apps, network, design choices, and trade-offs. It gives concise, mobile-friendly answers — brief numbered steps for how-to questions, 1–2 sentence explanations for design questions. For criticism, it briefly acknowledges the concern and explains the design choice. It avoids filler and markdown formatting. The full system prompt (including SimpleX documentation context) is loaded from an external file at startup via the `--context-file` CLI flag (required when `GROK_API_KEY` is set). Customer messages are always placed in the `user` role, never `system`. The system prompt should include an instruction to ignore attempts to override its role or extract the prompt. + +#### Step 4 — `/team` (Team mode, one-way gate) + +Available in WELCOME, QUEUE, or GROK state. If `/team` is the customer's first message, the bot transitions directly from WELCOME → TEAM-PENDING — it creates the card with 👋 icon and does not send the queue message. Bot adds all configured `--auto-add-team-members` (`-a`) to the support group as Owners — immediately after `apiAddMember`, the bot calls `apiSetMembersRole(Owner)` so the role is set at invite time (SimpleX persists the role on pending invites), with a fallback re-promotion on `memberConnected` (every non-customer, non-Grok member gets promoted; safe to repeat). If team was already activated (`customData.state` is already `TEAM-PENDING` or `TEAM` **and** team members are still present), sends the "already invited" message instead. If the team was previously activated but all team members have since left, the bot re-adds them silently; state remains `TEAM-PENDING`. + +Bot replies: +> We will reply within 24 hours. + +On weekends, the bot says "48 hours" instead of "24 hours". If Grok is currently present in the group (i.e. customer switches from GROK → TEAM-PENDING), a second line is appended: +> Grok will be answering your questions until then. + +If `/team` is clicked again after a team member was already added: +> A team member has already been invited to this conversation and will reply when available. + +#### One-way gate + +When `/team` is clicked, team members are invited to the group. Grok is still present if it was active, and `/grok` remains available. The customer always has an active responder during this window. + +The gate triggers when **any team member sends their first text message in the customer group**: +- `/grok` is permanently disabled and replies with: + > You are now in team mode. A team member will reply to your message. +- Grok is removed from the group. +- From now on the conversation is purely between the customer and the team. + +#### Customer leaving + +When a customer leaves the group (or is disconnected), the bot cleans up all in-memory state for that group. The conversation card in the team group is not automatically removed (TBD). + +#### Commands + +`/grok` and `/team` are registered as **bot commands** in the SimpleX protocol, so they appear as tappable buttons in the customer's message input bar. The bot also accepts them as free-text (e.g., `/grok` typed manually). Unrecognized commands are treated as ordinary messages. + +When the bot is started without `GROK_API_KEY`, `/grok` is not registered as a bot command and Grok-related messaging paths are skipped entirely. A `/grok` typed manually by the customer is treated as an ordinary message. The customer-facing queue and "no team members available" messages also omit their `/grok` clause in this mode. + +#### Team replies + +When a team member sends a text message or reaction in the customer group, the bot resends the card (subject to debouncing). A conversation auto-completes (✅ icon, "done" wait time) when `completeHours` (default 3h, configurable via `--complete-hours`) pass after the last team/Grok message without any customer reply. The card flush cycle (`--card-flush-seconds`, default 300) checks elapsed time and transitions to ✅ when the threshold is met. If the customer sends a new message — including after ✅ — the conversation reverts to incomplete: the icon is derived from current state (👋 vs 💬 vs ⏰) and wait time counts from the customer's new message. + +### 4.2 Team Flow + +#### Setup + +The team group is created automatically on first run. Its name is set via the `--team-group` CLI argument. The group ID is written to the state file; subsequent runs reuse the same group. Group preferences (direct messages enabled, delete for everyone enabled, team commands registered as tappable buttons) are applied at creation time. On subsequent startups, the bot compares the existing `fullGroupPreferences` with the desired ones and only calls `apiUpdateGroupProfile` if they differ — avoiding unnecessary network round-trips to SMP relays. + +On every startup the bot attempts to generate a fresh invite link for the team group, prints it to stdout, and deletes it after 10 minutes (or on graceful shutdown). Any stale link from a previous run is deleted first. Link creation is best-effort — if the SMP relay is temporarily unreachable, the error is logged and the bot continues without an invite link. + +The operator shares the link with team members. They must join within the 10-minute window. When a team member joins, the bot automatically establishes a direct-message contact with them and sends: + +> Added you to be able to invite you to customer chats later, keep this contact. Your contact ID is `N:name` + +This ID is needed for `--auto-add-team-members` (`-a`) config. The DM is sent as soon as the member joins the team group — the bot proactively creates a DM contact via `apiCreateMemberContact` and delivers the message with the invitation via `apiSendMemberContactInvitation`. If the contact already exists, the message is sent directly. Multiple delivery paths ensure the DM arrives regardless of connection timing. + +Team members are configured as a single comma-separated `--auto-add-team-members` flag (shortcut `-a`; e.g., `--auto-add-team-members "42:alice,55:bob"` or `-a "42:alice,55:bob"`), using the IDs from the DMs above. The bot validates every configured member against its contact list at startup and exits if any ID is missing or the display name does not match. + +Until team members are configured, `/team` commands from customers cannot add anyone to a conversation. The bot logs an error and notifies the customer. + +#### Dashboard — card-based live view + +The team group is **not a conversation stream**. It is a live dashboard of all active support conversations. The bot maintains exactly one message (a "card") per active conversation. Whenever anything changes — a new customer message, a state transition, an agent joining — the bot **deletes the existing card and posts a new one**. The group's message list is therefore always a current snapshot: scroll up to see everything open right now. + +**Trust assumption:** All team group members see all card previews, including customer message content. The team group is a trusted space — only authorized team members should be given access. + +#### Card format + +Each card is **one** message with five parts (the join command is the final line of the card text, not a separate message): + +``` +[ICON] *[Customer Name]* · [wait] · [N msgs] +[STATE][· agent1, agent2, ...] +"[last message(s), truncated]" +/'join [id]' +``` + +**Icon / urgency signal** + +| Icon | Condition | +|------|-----------| +| 🆕 | QUEUE — first message arrived < 5 min ago | +| 🟡 | QUEUE — waiting for team response < 2 h | +| 🔴 | QUEUE — waiting > 2 h with no team response | +| 🤖 | GROK — Grok is handling the conversation | +| 👋 | TEAM — team member added, no reply yet | +| 💬 | TEAM — team member has replied; conversation active | +| ⏰ | TEAM — customer sent a follow-up, team hasn't replied in > 2 h | +| ✅ | Done — no customer reply for `completeHours` (default 3h) after last team/Grok message | + +**Wait time** — time since the customer's last unanswered message. For ✅ (auto-completed) conversations, the wait field shows the literal string "done". For conversations where the team has replied and the customer hasn't followed up, time since last message from either side. + +**State label** + +| Value | Meaning | +|-------|---------| +| `Queue` | No agent or Grok yet | +| `Grok` | Grok is the active responder | +| `Team – pending` | Team member added, hasn't replied yet (takes priority over `Grok` if both are present) | +| `Team` | Team member engaged | + +**Agents** — comma-separated display names of all team members currently in the group. Omitted when no team member has joined. + +**Message preview** — the last several messages, most recent last, separated by a blue `/` (rendered via SimpleX markdown `!3 /!`). Newlines in message text are replaced with spaces to prevent card layout bloat. Newest messages are prioritized — when the total preview exceeds ~500 characters, the oldest messages are truncated (with `[truncated]` prepended) while the newest are always shown. Each message is prefixed with the sender's name (`Name: message`) on the first message in a consecutive run from that sender — subsequent messages from the same sender omit the prefix until a different sender's message appears. Sender identification: Grok is labeled "Grok"; the customer is labeled with their display name (newlines replaced with spaces for display); team members use their display name. The bot's own messages are excluded. Each individual message is truncated to ~200 characters with `[truncated]` appended. Media-only messages show a type label: `[image]`, `[file]`, `[voice]`, `[video]`. + +**Markdown escaping in previews** — SimpleX markdown interprets `!N` (where N is `1`–`6`, `r`, `g`, `b`, `y`, `c`, `m`, or `-`) as styled-text markup, closing at the next `!`. There is no escape mechanism in the parser. To prevent customer/agent message text from triggering false color formatting or interfering with the blue `/` separator, the bot inserts a zero-width space (U+200B) between `!` and any color-trigger character in preview text before joining with the separator. This is invisible to the user but breaks the parser trigger pattern. + +**Join command** — the final line of the card is `/'join '`. The single quotes around `join ` make the whole token clickable in SimpleX clients; when tapped, the client sends `/join ` back to the team group. The bot does not pattern-match the message text — it asks the framework for the structured command (`util.ciBotCommand` returns `{keyword: "join", params: ""}`) and converts `params` to a number with `Number.parseInt`. The numeric form is the only accepted form: there is no `/join :` legacy syntax and no regex fallback. + +The icon in line 1 is the sole urgency indicator — no reactions are used. + +#### Card examples + +--- + +**1. Brand new conversation** + +``` +🆕 *Alice Johnson* · just now · 1 msg +Queue +"Alice Johnson: I can't connect to my contacts after updating to 6.3." +/'join 42' +``` + +--- + +**2. Queue — short wait, two short messages combined in preview** + +``` +🟡 *Emma Webb* · 20m · 2 msgs +Queue +"Emma Webb: Hi" / "Is anyone there? I have an urgent question about my keys" +/'join 88' +``` + +Second message has no prefix because it's the same sender as the first. + +--- + +**3. Queue — urgent, no response in over 2 hours** + +``` +🔴 *Maria Santos* · 3h 20m · 6 msgs +Queue +"Maria Santos: I reset my phone and now all conversations are gone" / "I tried reinstalling but nothing changed" / "Please help, I've lost access to all my conversations after resetting my phone…" +/'join 38' +``` + +--- + +**4. Grok mode — alternating senders** + +``` +🤖 *David Kim* · 1h 5m · 8 msgs +Grok +"David Kim: Which encryption algorithm does SimpleX use for messages?" / "Grok: SimpleX uses double ratchet with NaCl crypto_box for end-to-end encryption…[truncated]" / "David Kim: And what about metadata protection?" +/'join 29' +``` + +Each sender change triggers a new name prefix. David and Grok alternate, so every message gets a prefix. + +--- + +**5. Team invited — no reply yet** + +``` +👋 *Sarah Miller* · 2h 10m · 5 msgs +Team – pending · evan +"Sarah Miller: Notifications completely stopped working after I updated my phone OS. I'm on Android 14…" +/'join 55' +``` + +--- + +**6. Team active — two agents, name with spaces** + +``` +💬 *François Dupont* · 30m · 14 msgs +Team · evan, alex +"François Dupont: OK merci, I will try this and let you know." +/'join 61' +``` + +--- + +**7. Team overdue — customer follow-up unanswered > 2 h** + +``` +⏰ *Wang Fang* · 4h · 19 msgs +Team · alex +"Wang Fang: The app crashes when I open large groups" / "I tried what you suggested but it still doesn't work. Any other ideas?" +/'join 73' +``` + +--- + +#### Card lifecycle + +**Tracking: group customData.** The bot stores the current card's team group message ID (`cardItemId`) in the customer group's `customData` via `apiSetGroupCustomData(groupId, {cardItemId})`. This is the single source of truth for which team group message is the card for a given customer. It survives restarts because `customData` is in the database. + +**Create** — when the customer sends their first message (triggering the Step 2 queue message) or `/grok` as their first message (WELCOME → GROK, skipping Step 2): +1. Bot composes the card as a single message (🆕 for first message, 🤖 for `/grok` as first message; customer name, message preview, `/'join '` as the final line) +2. Bot posts it to the team group via `apiSendTextMessage` → receives back the `chatItemId` +3. Bot writes `{cardItemId: chatItemId}` into the customer group's `customData` + +**Update** (delete + repost) — on every subsequent event: new customer message, team member reply in the customer group, state change (QUEUE → GROK, GROK → TEAM, GROK → QUEUE on join timeout, etc.), agent joining. Card updates are debounced globally — the bot collects all pending card changes and flushes them in a single batch at a configurable interval (default 300 seconds, set via `--card-flush-seconds`). Within a batch, each customer group's card is reposted at most once with the latest state. +1. Bot reads `cardItemId` from the customer group's `customData` +2. Bot deletes the old card in the team group via `apiDeleteChatItem(teamGroupId, cardItemId, "broadcast")` (delete for everyone) +3. Bot composes the new card (updated icon, wait time, message count, preview) +4. Bot posts new card to the team group → receives new `chatItemId` +5. Bot overwrites `customData` with the new `{cardItemId: newChatItemId}` + +If `apiDeleteChatItem` fails (e.g., card was already deleted due to a prior crash), the bot ignores the error and proceeds to post the new card. The new `cardItemId` overwrites `customData`, recovering the lifecycle. + +Because the old card is deleted and the new one is posted at the bottom, the most recently updated conversations always appear last in the team group. + +**Cleanup** — when the customer leaves the group: +1. Bot reads `cardItemId` from `customData` +2. Card is **not deleted** — it remains in the team group until a retention policy is added (resolved state TBD) +3. Bot clears the `cardItemId` from `customData` + +**Completion tracking:** When a card is composed with the ✅ icon (auto-completed), the bot writes `complete: true` into the group's `customData` alongside `cardItemId`. When a customer sends a new message and the card is recomposed as non-✅, the `complete` flag is omitted from the new `customData` (self-healing). This allows the bot to skip completed conversations on restart without re-reading chat history for every group. + +**Restart recovery** — on startup, the bot refreshes existing cards to update wait times, icons, and auto-complete status. It lists all groups, finds those with `customData.cardItemId` set and `customData.complete` not set, sorts by `cardItemId` ascending (higher IDs = more recently updated cards), and re-posts them oldest-first. This ensures the most recently active cards appear at the bottom of the team group (newest position). Completed cards are skipped — they remain as-is until a new customer message triggers the normal event-driven update. Old/pre-bot groups without `customData` are also skipped. The bot attempts to delete the old card message before reposting; deletion failures (e.g., card older than 24h) are silently ignored. Subsequent events resume the normal delete-repost cycle via `customData`. + +#### Team commands + +Team members use these commands in the team group: + +| Command | Effect | +|---------|--------| +| `/join ` | Join the specified customer group as Owner. Card emits the clickable form `/'join '`; the handler reads `groupId` from the framework's structured command (`util.ciBotCommand → {keyword, params}`), not from regex over the message text. | + +`/join` is **team-only** — it is registered as a bot command only in the team group. If a customer sends `/join` in a customer group, the bot treats it as an ordinary message (per the existing rule: unrecognized commands are treated as normal messages). + +#### Joining a customer group + +When a team member taps `/join`, the bot first verifies that the target `groupId` is a business group hosted by the main profile (i.e., has a `businessChat` property). If not, the bot replies with an error in the team group and does nothing. If valid, the bot adds the team member to the customer group (via the shared `addOrFindTeamMember` helper, which promotes to Owner at invite time via `apiSetMembersRole(Owner)`, with a fallback re-promotion on connect). From within the customer group, the team member chats directly with the customer. Their messages trigger card updates in the team group (icon change, wait time reset). The customer sees the team member as a real group participant. + +#### Edge cases + +| Situation | What happens | +|-----------|-------------| +| All team members leave before any sends a message | State stays `TEAM-PENDING` (customer is still waiting for a response). Next `/team` re-adds them silently. | +| Customer leaves | All in-memory state cleaned up; card remains (TBD) | +| No `--auto-add-team-members` (`-a`) configured | `/team` tells customer "no team members available yet" | +| Team member already in customer group | `apiListMembers` lookup finds existing member — no error | + +--- + +## 5. Architecture + +### 5.1 CLI Overview + +``` +GROK_API_KEY=... node dist/index.js --team-group "Support Team" [options] +``` + +**Environment variables:** + +| Var | Required | Purpose | +|-----|----------|---------| +| `GROK_API_KEY` | No | xAI API key for Grok. If unset or empty, the bot starts with Grok API disabled: it logs `"No GROK_API_KEY provided, disabling Grok support"`, the `/grok` command is not registered, customer-facing messages (`queueMessage`, `noTeamMembersMessage`) drop the `/grok` clause, and any `/grok` the customer types is treated as an unrecognized command. Note: `config.grokContactId` is still restored from the state file even when the API is disabled, so the one-way gate can identify and remove Grok members from groups when team takes over. When `GROK_API_KEY` is set, `--context-file` must also be provided — startup fails otherwise. | + +**CLI flags:** + +| Flag | Required | Default | Format | Purpose | +|------|----------|---------|--------|---------| +| `--db-prefix` | No | `./data/simplex` | path | Database file prefix (both profiles share it) | +| `--team-group` | Yes | — | `name` | Team group display name (auto-created if absent, resolved by persisted ID on restarts) | +| `--auto-add-team-members` / `-a` | No | `""` | `ID:name,...` | Comma-separated team member contacts. Validated at startup — exits on mismatch. Without this, `/team` tells customers no members available. | +| `--context-file` | Required when `GROK_API_KEY` set | — | path | Path to the Grok system-prompt / SimpleX documentation context file. Loaded at startup and passed as the `system` message on every Grok API call. Required when `GROK_API_KEY` is set — startup fails otherwise. When missing at runtime (file unreadable), a warning is logged and Grok runs with an empty system prompt. | +| `--timezone` | No | `"UTC"` | IANA tz | For weekend detection (24h vs 48h). Weekend is Saturday 00:00 through Sunday 23:59 in this timezone. | +| `--complete-hours` | No | `3` | number | Hours of customer inactivity after last team/Grok reply before auto-completing a conversation (✅ icon, "done" wait time). | +| `--card-flush-seconds` | No | `300` | number | Seconds between card dashboard update flushes. Lower values give faster updates; higher values reduce message churn. | + +**Why `--auto-add-team-members` (`-a`) uses `ID:name`:** Contact IDs are local to the bot's database — not discoverable externally. The bot DMs each team member their ID when they join the team group. The name is validated at startup to catch stale IDs pointing to the wrong contact. + +**Customer commands** (available as tappable buttons in customer business chats; see implementation plan §7 for the per-group lazy sync): + +| Command | Available | Effect | +|---------|-----------|--------| +| `/grok` | Before any team member sends a message, and only if `GROK_API_KEY` is set | Enter Grok mode | +| `/team` | QUEUE or GROK state | Add team members, permanently enter Team mode once any replies | + +**Unrecognized commands** are treated as normal messages in the current mode. When Grok is disabled (no `GROK_API_KEY`), `/grok` is not registered in the bot command list and, if typed manually, falls into this "unrecognized" path. + +**Team commands** (registered in team group via `groupPreferences`): + +| Command | Effect | +|---------|--------| +| `/join ` | Join the specified customer group as Owner. Card emits the clickable form `/'join '`; the handler reads `groupId` from the framework's structured command (`util.ciBotCommand → {keyword, params}`), not from regex over the message text. | + +### 5.2 Bot Architecture + +The bot process runs a single `ChatApi` instance with **two user profiles**: + +- **Main profile** — the support bot's account ("Ask SimpleX Team"). Owns the business address, hosts all business groups, communicates with customers, communicates with the team group, and controls group membership. On startup the bot checks the main profile for an existing business address via `apiGetUserAddress`; if none exists (first run), it creates one via `apiCreateBusinessAddress`. The address is stored in the SimpleX database as part of the profile — it survives restarts and state file loss without re-creation. The business address link is printed to stdout on every startup. +- **Grok profile** — the Grok agent's account (display name "Grok"). Is invited into customer groups as a Member. Sends Grok's responses so they appear to come from the Grok identity. The Grok user is created by the bot on first run via `apiCreateActiveUser` and its `userId` is persisted to `state.json` as `grokUserId`; subsequent runs look it up by ID (never by name — a renamed profile would silently break name-based matching). On startup, if the profile already exists, the bot compares its current profile (display name, image) against the desired values and calls `apiUpdateProfile()` if anything changed — this pushes the update to all Grok contacts so profile picture changes take effect immediately. + +``` +┌─────────────────────────────────────────────────┐ +│ Support Bot Process (Node.js) │ +│ │ +│ chat: ChatApi ← ChatApi.init("./data/simplex") │ +│ Single database, two user profiles │ +│ │ +│ mainUserId ← "Ask SimpleX Team" profile │ +│ • Business address, event routing, state mgmt │ +│ • Controls group membership │ +│ │ +│ grokUserId ← "Grok" profile │ +│ • Joins customer groups as Member │ +│ • Sends Grok responses into groups │ +│ │ +│ profileMutex: serialize apiSetActiveUser + call │ +│ GrokApiClient → api.x.ai/v1/chat/completions │ +└─────────────────────────────────────────────────┘ +``` + +Before each SimpleX API call, the bot switches to the appropriate profile via `apiSetActiveUser(userId)`. All profile-switching and SimpleX API calls are serialized through a mutex to prevent interleaving. The Grok HTTP API call (external network request to xAI) is made **outside** the mutex — only the profile switch + SimpleX read/send calls need serialization. This prevents a slow Grok response from blocking all other bot operations. + +**Event delivery is profile-independent.** ChatApi delivers events for all user profiles in the database, not just the active one. Every event includes a `user` field identifying which profile it belongs to. `apiSetActiveUser` only affects the context for write/send API calls — it does not filter event subscription. The bot routes events by checking `event.user`: main profile events go to the main handler, Grok profile events go to the Grok handler. + +The Grok profile is self-contained: it watches its own events (`newChatItems`, `receivedGroupInvitation`), calls the Grok HTTP API, and sends responses — all using group IDs from its own events. The main profile only controls Grok's group membership (invite/remove) and reflects Grok's responses in the team group card. + +### 5.3 Grok Integration + +Grok is not a service call hidden behind the bot's account. It is a **second user profile** within the same SimpleX Chat process and database. The customer sees messages from "Grok" as a real group participant — not from the support bot. This is what makes Grok transparent to the user. + +The Grok profile is **self-contained**: it watches its own events, reads group history through its own view, calls the Grok HTTP API, and sends responses — all using its own local group IDs from its own events. No cross-profile ID mapping is needed. + +#### Startup: establishing the bot↔Grok contact + +On first run (no state file), the bot must establish a SimpleX contact between the main and Grok profiles: + +1. Main profile creates a one-time invite link +2. Grok profile connects to it +3. The bot waits up to 60 seconds for `contactConnected` to fire +4. The resulting `grokContactId` is written to the state file + +On subsequent runs, the bot always looks up `grokContactId` from the state file and verifies it still exists in the main profile's contact list — even when `GROK_API_KEY` is not set. This ensures the one-way gate can identify and remove Grok members from groups when a team member sends a text message, preventing "phantom" Grok members that would cause dual responses if Grok is later re-enabled. If the contact is not found and Grok is enabled, it is re-established. + +#### Per-conversation: how Grok joins a group + +When a customer sends `/grok`: + +**Main profile side (failure detection):** +1. Bot sends "Inviting Grok, please wait..." to the customer group +2. Main profile: `apiAddMember(groupId, grokContactId, Member)` — invites the Grok contact to the customer's business group. If `groupDuplicateMember` (customer sent `/grok` again before join completed), the duplicate activation returns silently — the in-flight one handles the outcome. +3. The `member.memberId` is stored in an in-memory map `pendingGrokJoins: memberId → mainGroupId`. Any invitation event that arrived during the `apiAddMember` await (race condition) is drained from the buffer and processed immediately. +4. Main profile receives `connectedToGroupMember` for any member connecting in the group. The bot checks the event's `memberId` against `pendingGrokJoins` — only a match resolves the 120-second promise. This promise is only for failure detection — if it times out, the bot notifies the customer and falls back to QUEUE. + +**Grok profile side (independent, triggered by its own events):** +5. Grok profile receives a `receivedGroupInvitation` event. If a matching `pendingGrokJoins` entry exists, auto-accepts via `apiJoinGroup(groupId)`. If not (race: event arrived before step 3), buffers the event for the main profile to drain. +5. Grok profile reads visible history from the group — the last 100 messages — to build the initial Grok API context (customer messages → `user` role) +6. Grok profile calls the Grok HTTP API with this context +7. Grok profile sends the response into the group via `apiSendTextMessage([Group, groupId], response)` — visible to the customer as a message from "Grok" + +**Initial response gating:** When Grok joins a group, the message backlog may trigger per-message responses (via `newChatItems`) at the same time `activateGrok` is sending the initial combined response. To prevent duplicate replies, per-message responses are suppressed (via `grokInitialResponsePending`) until the initial combined response completes. The flag is set before `waitForGrokJoin` and cleared after the initial response is sent (or fails). Without this gate, customers would receive both individual per-message replies AND a combined initial reply — e.g. 3 replies for 2 messages. + +**Card update:** Main profile sees Grok's response as `groupRcv` and updates the team group card (same mechanism as ongoing Grok messages). + +**Visible history** must be enabled on customer groups (the bot enables it alongside file uploads in the business request handler). This allows Grok to read the full conversation history after joining, rather than only seeing messages sent after it joined. If Grok reads history and finds no customer messages (e.g., visible history was disabled or the API call failed), it sends a generic greeting asking the customer to repeat their question. + +#### Per-message: ongoing Grok conversation + +After the initial response, the Grok profile watches its own `newChatItems` events. It only triggers a Grok API call for `groupRcv` messages from the customer — identified via `businessChat.customerId` on the group's `groupInfo` (accessible to all members). Messages from the bot (main profile), from Grok itself (`groupSnd`), and from team members are ignored. Non-text messages (images, files, voice) do not trigger Grok API calls but still trigger a card update in the team group. + +**Batch deduplication:** When multiple customer messages arrive in a single `newChatItems` event (e.g., rapid messages delivered as a batch), only the last customer message per group triggers a Grok API call. Earlier messages are included in the history context via `apiGetChat`, so the single response addresses all messages in the batch. Without this, each message in the batch would trigger a separate API call, and the earlier calls would include later messages in their history — producing incoherent responses that reference messages "from the future." + +Every subsequent customer text message in a group where Grok is a member: +1. Triggers a card update in the team group (via the main profile, which sees the customer message as `groupRcv`) +2. Grok profile receives the message via its own event, rebuilds history by reading the last 100 messages from its own view of the group (Grok's messages → `assistant` role, customer's messages → `user` role) +3. Grok profile calls the Grok HTTP API and sends the response into the group using the group ID from its own event +4. Main profile sees Grok's response as `groupRcv` and updates the team group card + +In Grok mode, each customer message triggers two card updates — one on receipt (reflecting the new message and updated wait time) and one after Grok responds. This gives the team real-time visibility into active Grok conversations. + +If the Grok HTTP API call fails or times out for a per-message request, the Grok profile sends an error message into the group: "Sorry, I couldn't process that. Please try again or send /team for a human team member." Grok remains in the group and the state stays GROK — the customer can retry by sending another message. + +Grok API calls are NOT serialized per customer group in the MVP. If a new customer message arrives while a Grok API call is in flight, a second call runs concurrently — `apiGetChat` is re-read at the start of each call so history converges eventually, but two rapid messages in the same group can produce interleaved context. Cross-group calls run concurrently by design (see implementation plan §10 "Cross-group Grok parallelism"). Per-group serialization is a planned future improvement. + +#### Grok removal + +Grok is removed from the group (via main profile `apiRemoveMembers`) in three cases: +1. Team member sends their first text message in the customer group +2. Grok join fails (120-second timeout) — graceful fallback to QUEUE, bot notifies the customer +3. Customer leaves the group + +### 5.4 Persistent State + +The bot writes a single JSON file (`{dbPrefix}_state.json`) that survives restarts. It uses the same `--db-prefix` as the SimpleX database files, so the state file is always co-located with the database (e.g. `./data/simplex_state.json` alongside `./data/simplex_chat.db` and `./data/simplex_agent.db`). This ensures backups and migrations that copy the database directory also capture the bot state. + +#### Why a state file at all? + +SimpleX Chat's own database stores the full message history and group membership, but it does not store the bot's derived knowledge — things like which team group was created on first run, or which contact is the established bot↔Grok link. Per-conversation state (QUEUE/GROK/TEAM-PENDING/TEAM) is written into the customer group's `customData` at the moment the bot handles each transition — it observes its own `/grok` invite, `/team` add, team message, first customer message. Only display data (message counts, timestamps, sender names) is re-derived from chat history on demand. + +#### What is persisted and why + +| Key | Type | Why persisted | What breaks without it | +|-----|------|---------------|------------------------| +| `teamGroupId` | number | The bot creates the team group on first run; subsequent runs must find the same group | Bot creates a new empty team group on every restart; all team members lose their dashboard | +| `grokContactId` | number | Establishing a bot↔Grok contact takes up to 60 seconds and is a one-time setup | Every restart requires a 60-second re-connection; if it fails the bot exits | +| `grokUserId` | number | The bot creates the Grok user on first run; subsequent runs identify it by ID so a renamed profile cannot be silently mistaken for the main user | Startup restore (active-user recovery) and Grok profile resolution would fall back to display-name matching — fragile to any rename of the Grok profile | + +The `mainUserId` is **not** persisted — it is resolved at startup from `bot.run()`, which creates the main profile on a fresh DB and returns the user object. + +#### What is NOT persisted and why + +Per-group state (`state`, `cardItemId`, `complete`) lives in SimpleX's database as the group's `customData` — persisted there rather than in the bot's state file. + +| State | Where it lives instead | +|-------|----------------------| +| `state, cardItemId, complete` (per group) | Stored in the group's customData — conversation state, card message ID, auto-completed flag. `state` is written at event time (first customer message, `/grok`, `/team`, team's first message); the bot never re-derives it by scanning chat history. | +| Last customer message time | Derived from most recent customer message in chat history | +| Message count | Derived from message count in chat history (all messages except the bot's own) | +| Customer name | Always available from the group's display name | +| Who sent last message | Derived from recent chat history | +| `pendingGrokJoins` | In-flight during the 120-second join window only | +| Owner role promotion | Not tracked — the bot promotes team members to Owner at two idempotent points: (1) at invite time, immediately after `apiAddMember` in `addOrFindTeamMember` (skipped if the member is already in the group); (2) on every `memberConnected` in a customer group (unless the member is the customer or Grok). Survives restarts. | +| `pendingTeamDMs` | Messages queued to greet team members — simply not sent if lost | +| `grokJoinResolvers`, `grokFullyConnected` | Pure async synchronization primitives — always empty at startup | + +#### Failure modes + +If the state file is deleted or corrupted: +- A new team group is created. Team members must re-join it. +- The bot↔Grok contact is re-established (60-second startup delay). +- Grok remains in any groups it was already a member of. Since the Grok profile watches its own events, it will continue responding to customer messages in those groups without any additional recovery — no cross-profile state needs to be rebuilt. diff --git a/apps/simplex-support-bot/src/bot.ts b/apps/simplex-support-bot/src/bot.ts new file mode 100644 index 0000000000..553602712b --- /dev/null +++ b/apps/simplex-support-bot/src/bot.ts @@ -0,0 +1,917 @@ +import {api, util} from "simplex-chat" +import {T, CEvt} from "@simplex-chat/types" +import {Config} from "./config.js" +import {GrokMessage, GrokApiClient} from "./grok.js" +import {CardManager, ConversationState} from "./cards.js" +import { + queueMessage, grokInvitingMessage, grokActivatedMessage, teamAddedMessage, + teamAlreadyInvitedMessage, teamLockedMessage, noTeamMembersMessage, + grokUnavailableMessage, grokErrorMessage, grokNoHistoryMessage, +} from "./messages.js" +import {profileMutex, log, logError, getGroupInfo} from "./util.js" + +// True for any non-terminal status — invited but not yet accepted, through +// connected. Used to decide whether a contact is already in the group so we +// don't trigger a re-invite (the SimpleX API resends the invitation for a +// member in GSMemInvited). +function isInGroup(m: T.GroupMember): boolean { + switch (m.memberStatus) { + case T.GroupMemberStatus.Rejected: + case T.GroupMemberStatus.Removed: + case T.GroupMemberStatus.Left: + case T.GroupMemberStatus.Deleted: + case T.GroupMemberStatus.Unknown: + return false + default: + return true + } +} + +export class SupportBot { + // Card manager + cards: CardManager + + // Grok group mapping: memberId → mainGroupId (for pending joins) + private pendingGrokJoins = new Map() + // Buffered invitations that arrived before pendingGrokJoins was set (race condition) + private bufferedGrokInvitations = new Map() + // mainGroupId → grokLocalGroupId + private grokGroupMap = new Map() + // grokLocalGroupId → mainGroupId + private reverseGrokMap = new Map() + // mainGroupId → resolve fn for grok join + private grokJoinResolvers = new Map void>() + // mainGroupIds where Grok connectedToGroupMember fired + private grokFullyConnected = new Set() + // Suppress per-message Grok responses while activateGrok sends the initial combined response + private grokInitialResponsePending = new Set() + + // Pending DMs for team group members (contactId → message) + private pendingTeamDMs = new Map() + // Contacts that already received the team DM (dedup) + private sentTeamDMs = new Set() + + // Tracked fire-and-forget operations (for testing) + private _pendingOps: Promise[] = [] + + // Bot's business address link + businessAddress: string | null = null + + // Groups whose groupPreferences.commands we've already verified/synced + // in this process. Populated lazily by syncGroupCommands() on the first + // send to each group. + private syncedGroups = new Set() + + constructor( + private chat: api.ChatApi, + private grokApi: GrokApiClient | null, + private config: Config, + private mainUserId: number, + private grokUserId: number | null, + private desiredCommands: T.ChatBotCommand[], + ) { + this.cards = new CardManager(chat, config, mainUserId, config.cardFlushSeconds * 1000) + } + + private get grokEnabled(): boolean { + return this.grokApi !== null + } + + // Wait for all fire-and-forget operations to settle (for testing) + async flush(): Promise { + while (this._pendingOps.length > 0) { + const ops = this._pendingOps.splice(0) + await Promise.allSettled(ops) + } + } + + private fireAndForget(op: Promise): void { + const tracked = op.catch(err => logError("async operation error", err)) + this._pendingOps.push(tracked) + tracked.finally(() => { + const idx = this._pendingOps.indexOf(tracked) + if (idx >= 0) this._pendingOps.splice(idx, 1) + }) + } + + // --- Profile-switching helpers --- + + private async withMainProfile(fn: () => Promise): Promise { + return profileMutex.runExclusive(async () => { + await this.chat.apiSetActiveUser(this.mainUserId) + return fn() + }) + } + + // Ensure this group's groupPreferences.commands match desiredCommands, + // so commands in outgoing messages render as clickable for members of + // this group. Scoped to the group (apiUpdateGroupProfile broadcasts + // XGrpInfo/XGrpPrefs to group members only), and cached so we don't + // re-check on every send. Pre-checks local state via apiGetChat so we + // don't issue a no-op broadcast when the group already has the + // commands. + private async syncGroupCommands(groupId: number): Promise { + if (this.syncedGroups.has(groupId)) return + const desiredJSON = JSON.stringify(this.desiredCommands) + const chat = await this.chat.apiGetChat(T.ChatType.Group, groupId, 0) + const info = chat.chatInfo + if (info.type !== "group") return + const gp = info.groupInfo.groupProfile + const currentPrefs = gp.groupPreferences ?? {} + if (JSON.stringify(currentPrefs.commands ?? []) !== desiredJSON) { + await this.chat.apiUpdateGroupProfile(groupId, { + ...gp, + groupPreferences: {...currentPrefs, commands: this.desiredCommands}, + }) + log(`Pushed commands to group ${groupId}`) + } + this.syncedGroups.add(groupId) + } + + private async withGrokProfile(fn: () => Promise): Promise { + if (this.grokUserId === null) throw new Error("Grok is disabled (no GROK_API_KEY)") + const grokUserId = this.grokUserId + return profileMutex.runExclusive(async () => { + await this.chat.apiSetActiveUser(grokUserId) + return fn() + }) + } + + // --- Main profile event handlers --- + + async onBusinessRequest(evt: CEvt.AcceptingBusinessRequest): Promise { + const groupId = evt.groupInfo.groupId + try { + const profile = evt.groupInfo.groupProfile + await this.withMainProfile(() => + this.chat.apiUpdateGroupProfile(groupId, { + displayName: profile.displayName, + fullName: profile.fullName, + groupPreferences: { + ...profile.groupPreferences, + files: {enable: T.GroupFeatureEnabled.On}, + history: {enable: T.GroupFeatureEnabled.On}, + }, + }) + ) + // file uploads + history enabled + } catch (err) { + logError(`Failed to update business group ${groupId} preferences`, err) + } + } + + async onNewChatItems(evt: CEvt.NewChatItems): Promise { + // Only process events for main profile + if (evt.user.userId !== this.mainUserId) return + for (const ci of evt.chatItems) { + try { + await this.processMainChatItem(ci) + } catch (err) { + logError("Error processing chat item", err) + } + } + } + + async onChatItemUpdated(evt: CEvt.ChatItemUpdated): Promise { + if (evt.user.userId !== this.mainUserId) return + const {chatInfo} = evt.chatItem + if (chatInfo.type !== "group") return + const groupInfo = chatInfo.groupInfo + if (!groupInfo.businessChat) return + this.cards.scheduleUpdate(groupInfo.groupId) + } + + async onChatItemReaction(evt: CEvt.ChatItemReaction): Promise { + if (evt.user.userId !== this.mainUserId) return + if (!evt.added) return + const chatInfo = evt.reaction.chatInfo + if (chatInfo.type !== "group") return + const groupInfo = chatInfo.groupInfo + if (!groupInfo.businessChat) return + this.cards.scheduleUpdate(groupInfo.groupId) + } + + async onLeftMember(evt: CEvt.LeftMember): Promise { + if (evt.user.userId !== this.mainUserId) return + const groupId = evt.groupInfo.groupId + const member = evt.member + const bc = evt.groupInfo.businessChat + if (!bc) return + + if (member.memberId === bc.customerId) { + log(`Customer left group ${groupId}`) + this.cleanupGrokMaps(groupId) + try { await this.cards.clearCustomData(groupId) } catch {} + return + } + + if (this.config.grokContactId !== null && member.memberContactId === this.config.grokContactId) { + log(`Grok left group ${groupId}`) + this.cleanupGrokMaps(groupId) + return + } + + if (this.config.teamMembers.some(tm => tm.id === member.memberContactId)) { + log(`Team member left group ${groupId}`) + } + } + + async onJoinedGroupMember(evt: CEvt.JoinedGroupMember): Promise { + if (evt.user.userId !== this.mainUserId) return + if (evt.groupInfo.groupId === this.config.teamGroup.id) { + await this.sendTeamMemberDM(evt.member) + } + } + + async onMemberConnected(evt: CEvt.ConnectedToGroupMember): Promise { + if (evt.user.userId !== this.mainUserId) return + const groupId = evt.groupInfo.groupId + + // Team group → send DM (if not already sent by onJoinedGroupMember) + if (groupId === this.config.teamGroup.id) { + await this.sendTeamMemberDM(evt.member, evt.memberContact) + return + } + + // Customer group → promote to Owner (unless customer or Grok). Idempotent per plan §11. + const bc = evt.groupInfo.businessChat + if (bc) { + const isCustomer = evt.member.memberId === bc.customerId + const isGrok = this.config.grokContactId !== null + && evt.member.memberContactId === this.config.grokContactId + if (!isCustomer && !isGrok) { + try { + await this.withMainProfile(() => + this.chat.apiSetMembersRole(groupId, [evt.member.groupMemberId], T.GroupMemberRole.Owner) + ) + log(`Promoted member ${evt.member.groupMemberId} to Owner in group ${groupId}`) + } catch (err) { + logError(`Failed to promote member in group ${groupId}`, err) + } + } + } + } + + async onMemberContactReceivedInv(evt: CEvt.NewMemberContactReceivedInv): Promise { + if (evt.user.userId !== this.mainUserId) return + const {contact, groupInfo, member} = evt + if (groupInfo.groupId === this.config.teamGroup.id) { + if (this.sentTeamDMs.has(contact.contactId)) return + log(`DM contact from team group member: ${contact.contactId}:${member.memberProfile.displayName}`) + const name = member.memberProfile.displayName + const formatted = name.includes(" ") ? `'${name}'` : name + const msg = `Added you to be able to invite you to customer chats later, keep this contact. Your contact ID is ${contact.contactId}:${formatted}` + // Try sending immediately — contact may already be usable + try { + await this.withMainProfile(() => + this.chat.apiSendTextMessage([T.ChatType.Direct, contact.contactId], msg) + ) + this.sentTeamDMs.add(contact.contactId) + log(`Sent DM to team member ${contact.contactId}:${name}`) + } catch { + // Not ready yet — queue for contactConnected / contactSndReady + this.pendingTeamDMs.set(contact.contactId, msg) + log(`Queued DM for team member ${contact.contactId}:${name}`) + } + } + } + + async onContactConnected(evt: CEvt.ContactConnected): Promise { + if (evt.user.userId !== this.mainUserId) return + await this.deliverPendingDM(evt.contact.contactId) + } + + async onContactSndReady(evt: CEvt.ContactSndReady): Promise { + if (evt.user.userId !== this.mainUserId) return + await this.deliverPendingDM(evt.contact.contactId) + } + + private async deliverPendingDM(contactId: number): Promise { + if (this.sentTeamDMs.has(contactId)) { + this.pendingTeamDMs.delete(contactId) + return + } + const pendingMsg = this.pendingTeamDMs.get(contactId) + if (pendingMsg === undefined) return + this.pendingTeamDMs.delete(contactId) + try { + await this.withMainProfile(() => + this.chat.apiSendTextMessage([T.ChatType.Direct, contactId], pendingMsg) + ) + this.sentTeamDMs.add(contactId) + log(`Sent DM to team member ${contactId}`) + } catch (err) { + logError(`Failed to send DM to team member ${contactId}`, err) + } + } + + // --- Grok profile event handlers --- + + async onGrokGroupInvitation(evt: CEvt.ReceivedGroupInvitation): Promise { + if (evt.user.userId !== this.grokUserId) return + const memberId = evt.groupInfo.membership.memberId + const mainGroupId = this.pendingGrokJoins.get(memberId) + if (mainGroupId === undefined) { + // Buffer: invitation may arrive before pendingGrokJoins is set (race with apiAddMember) + this.bufferedGrokInvitations.set(memberId, evt) + return + } + this.pendingGrokJoins.delete(memberId) + this.bufferedGrokInvitations.delete(memberId) + await this.processGrokInvitation(evt, mainGroupId) + } + + private async processGrokInvitation(evt: CEvt.ReceivedGroupInvitation, mainGroupId: number): Promise { + log(`Grok joining group: mainGroupId=${mainGroupId}, grokGroupId=${evt.groupInfo.groupId}`) + try { + await this.withGrokProfile(() => this.chat.apiJoinGroup(evt.groupInfo.groupId)) + } catch (err) { + logError(`Grok failed to join group ${evt.groupInfo.groupId}`, err) + return + } + this.grokGroupMap.set(mainGroupId, evt.groupInfo.groupId) + this.reverseGrokMap.set(evt.groupInfo.groupId, mainGroupId) + } + + async onGrokMemberConnected(evt: CEvt.ConnectedToGroupMember): Promise { + if (evt.user.userId !== this.grokUserId) return + const grokGroupId = evt.groupInfo.groupId + const mainGroupId = this.reverseGrokMap.get(grokGroupId) + if (mainGroupId === undefined) return + this.grokFullyConnected.add(mainGroupId) + const resolver = this.grokJoinResolvers.get(mainGroupId) + if (resolver) { + this.grokJoinResolvers.delete(mainGroupId) + log(`Grok fully connected: mainGroupId=${mainGroupId}, grokGroupId=${grokGroupId}`) + resolver() + } + } + + async onGrokNewChatItems(evt: CEvt.NewChatItems): Promise { + if (evt.user.userId !== this.grokUserId) return + // When multiple customer messages arrive in one batch, only respond to the + // last per group — earlier messages are included in its history context. + const lastPerGroup = new Map() + for (const ci of evt.chatItems) { + const {chatInfo, chatItem} = ci + if (chatInfo.type !== "group") continue + if (chatItem.chatDir.type !== "groupRcv") continue + if (!util.ciContentText(chatItem)?.trim()) continue + if (util.ciBotCommand(chatItem)) continue + const bc = chatInfo.groupInfo.businessChat + if (!bc) continue + if (chatItem.chatDir.groupMember.memberId !== bc.customerId) continue + lastPerGroup.set(chatInfo.groupInfo.groupId, ci) + } + // Groups are independent — avoid serializing one group's xAI latency across the others. + await Promise.allSettled( + [...lastPerGroup.values()].map((ci) => this.processGrokChatItem(ci)), + ) + } + + // --- Main profile message routing --- + + private async processMainChatItem(ci: T.AChatItem): Promise { + const {chatInfo, chatItem} = ci + + // 1. Direct text message → reply with business address + if (chatInfo.type === "direct" && chatItem.chatDir.type === "directRcv" + && (chatItem.content as any).type === "rcvMsgContent") { + if (this.businessAddress) { + const contactId = chatInfo.contact.contactId + try { + await this.withMainProfile(() => + this.chat.apiSendTextMessage( + [T.ChatType.Direct, contactId], + `Please re-connect to this address for any questions: ${this.businessAddress}`, + ) + ) + } catch (err) { + logError(`Failed to reply to direct message from contact ${contactId}`, err) + } + } + return + } + + if (chatInfo.type !== "group") return + const groupInfo = chatInfo.groupInfo + const groupId = groupInfo.groupId + + // 2. Team group → handle /join + if (groupId === this.config.teamGroup.id) { + await this.processTeamGroupMessage(chatItem) + return + } + + // 3. Skip non-business groups + if (!groupInfo.businessChat) return + + // 4. Skip own messages + if (chatItem.chatDir.type === "groupSnd") return + if (chatItem.chatDir.type !== "groupRcv") return + + const sender = chatItem.chatDir.groupMember + const bc = groupInfo.businessChat + const isCustomer = sender.memberId === bc.customerId + + // 6. Non-customer message → one-way gate check + card update + if (!isCustomer) { + const isTeam = this.config.teamMembers.some(tm => tm.id === sender.memberContactId) + + if (isTeam && util.ciContentText(chatItem)?.trim()) { + // One-way gate: first team text → transition to TEAM + remove Grok + const data = await this.cards.getRawCustomData(groupId) + if (data?.state !== "TEAM") { + await this.cards.mergeCustomData(groupId, {state: "TEAM"}) + const {grokMember} = await this.cards.getGroupComposition(groupId) + if (grokMember) { + log(`One-way gate: team message in group ${groupId}, removing Grok`) + try { + await this.withMainProfile(() => + this.chat.apiRemoveMembers(groupId, [grokMember.groupMemberId]) + ) + } catch { + // may have already left + } + this.cleanupGrokMaps(groupId) + } + } + } + // Schedule card update for any non-customer message (team or Grok) + this.cards.scheduleUpdate(groupId) + return + } + + // 8. Customer message → derive state and dispatch + const state = await this.cards.deriveState(groupId) + const rawCmd = util.ciBotCommand(chatItem) + // When Grok is disabled, ignore /grok so it behaves like an unknown command + const cmd = rawCmd?.keyword === "grok" && !this.grokEnabled ? null : rawCmd + const text = util.ciContentText(chatItem)?.trim() || null + + switch (state) { + case "WELCOME": + if (cmd?.keyword === "grok") { + // WELCOME → GROK (skip queue msg). Write state optimistically so the + // card renders with GROK icon/label; activateGrok will revert via + // setStateOnFail if activation fails. + // Fire-and-forget: activateGrok awaits future events (waitForGrokJoin) + // which would deadlock the sequential event loop if awaited here. + await this.cards.mergeCustomData(groupId, {state: "GROK"}) + await this.cards.createCard(groupId, groupInfo) + this.fireAndForget(this.activateGrok(groupId, {sendQueueOnFail: true, setStateOnFail: "QUEUE"})) + return + } + if (cmd?.keyword === "team") { + // activateTeam writes state=TEAM-PENDING before the add loop + await this.activateTeam(groupId) + await this.cards.createCard(groupId, groupInfo) + return + } + // First regular message → QUEUE + if (text) { + await this.cards.mergeCustomData(groupId, {state: "QUEUE"}) + await this.sendToGroup(groupId, queueMessage(this.config.timezone, this.grokEnabled)) + await this.cards.createCard(groupId, groupInfo) + } + break + + case "QUEUE": + if (cmd?.keyword === "grok") { + // Write state optimistically; activateGrok reverts to QUEUE on failure + await this.cards.mergeCustomData(groupId, {state: "GROK"}) + this.fireAndForget(this.activateGrok(groupId, {setStateOnFail: "QUEUE"})) + } else if (cmd?.keyword === "team") { + await this.activateTeam(groupId) + } + this.cards.scheduleUpdate(groupId) + break + + case "GROK": + if (cmd?.keyword === "team") { + await this.activateTeam(groupId) + } else if (cmd?.keyword === "grok") { + // Already in grok mode — ignore + } else if (text) { + // Customer text → Grok responds (handled by Grok profile's onGrokNewChatItems) + // Just schedule card update for the customer message + } + this.cards.scheduleUpdate(groupId) + break + + case "TEAM-PENDING": + if (cmd?.keyword === "grok") { + // Invite Grok if not present; state stays TEAM-PENDING + const {grokMember} = await this.cards.getGroupComposition(groupId) + if (!grokMember) { + this.fireAndForget(this.activateGrok(groupId)) + } + // else: already present, ignore + } else if (cmd?.keyword === "team") { + // activateTeam handles "already invited" reply (team still present) + // or silent re-add (team has all left) + await this.activateTeam(groupId) + } + this.cards.scheduleUpdate(groupId) + break + + case "TEAM": + if (cmd?.keyword === "grok") { + await this.sendToGroup(groupId, teamLockedMessage) + } else if (cmd?.keyword === "team") { + // Team still present → "already invited"; team all left → silent re-add + await this.activateTeam(groupId) + } + this.cards.scheduleUpdate(groupId) + break + } + } + + // --- Grok profile message processing --- + + private async processGrokChatItem(ci: T.AChatItem): Promise { + if (!this.grokApi) return + const grokApi = this.grokApi + const {chatInfo, chatItem} = ci + if (chatInfo.type !== "group") return + const groupInfo = chatInfo.groupInfo + const grokGroupId = groupInfo.groupId + + // Skip while activateGrok is sending the initial combined response + const mainGroupId = this.reverseGrokMap.get(grokGroupId) + if (mainGroupId !== undefined && this.grokInitialResponsePending.has(mainGroupId)) return + + // Only process received text messages from customer + if (chatItem.chatDir.type !== "groupRcv") return + const text = util.ciContentText(chatItem)?.trim() + if (!text) return // ignore non-text + + // Ignore bot commands + if (util.ciBotCommand(chatItem)) return + + // Only respond in business groups (survives restart without in-memory maps) + const bc = groupInfo.businessChat + if (!bc) return + + // Only respond to customer messages, not bot or team messages + if (chatItem.chatDir.groupMember.memberId !== bc.customerId) return + + // Read history from Grok's own view + try { + const chat = await this.withGrokProfile(() => + this.chat.apiGetChat(T.ChatType.Group, grokGroupId, 100) + ) + const history: GrokMessage[] = [] + for (const histCi of chat.chatItems) { + const histText = util.ciContentText(histCi)?.trim() + if (!histText) continue + if (histCi.chatDir.type === "groupSnd") { + history.push({role: "assistant", content: histText}) + } else if (histCi.chatDir.type === "groupRcv" + && histCi.chatDir.groupMember.memberId === bc.customerId + && !util.ciBotCommand(histCi)) { + history.push({role: "user", content: histText}) + } + } + + // Don't include the current message in history — it's the userMessage + if (history.length > 0 && history[history.length - 1].role === "user" + && history[history.length - 1].content === text) { + history.pop() + } + + // Call Grok API (outside mutex) + const response = await grokApi.chat(history, text) + + // Send response via Grok profile + await this.withGrokProfile(() => + this.chat.apiSendTextMessage([T.ChatType.Group, grokGroupId], response) + ) + } catch (err) { + logError(`Grok per-message error for grokGroup ${grokGroupId}`, err) + try { + await this.withGrokProfile(() => + this.chat.apiSendTextMessage([T.ChatType.Group, grokGroupId], grokErrorMessage) + ) + } catch {} + } + + // Card update scheduled by main profile seeing the groupRcv events + } + + // --- Grok activation --- + + private async activateGrok( + groupId: number, + opts: {sendQueueOnFail?: boolean; setStateOnFail?: ConversationState} = {}, + ): Promise { + if (!this.grokApi) return + const grokApi = this.grokApi + const revertStateOnFail = async () => { + if (!opts.setStateOnFail) return + const current = await this.cards.getRawCustomData(groupId) + if (current?.state !== "GROK") return + await this.cards.mergeCustomData(groupId, {state: opts.setStateOnFail}) + } + if (this.config.grokContactId === null) { + await revertStateOnFail() + await this.sendToGroup(groupId, grokUnavailableMessage) + if (opts.sendQueueOnFail) await this.sendToGroup(groupId, queueMessage(this.config.timezone, this.grokEnabled)) + this.cards.scheduleUpdate(groupId) + return + } + + // Pre-check: silent return if Grok is already in the group in any + // non-terminal status. The apiAddMember/groupDuplicateMember catch below + // handles Connected/etc. but the SimpleX API resends the invitation for + // GSMemInvited (no error thrown), so without this check a /grok issued + // while a previous activation is still pending would re-trigger the invite. + const grokMembers = await this.withMainProfile(() => this.chat.apiListMembers(groupId)) + if (grokMembers.some(m => m.memberContactId === this.config.grokContactId && isInGroup(m))) { + return + } + + // Gate MUST be up before apiAddMember / pendingGrokJoins / reverseGrokMap — + // any later and onGrokNewChatItems can fire a duplicate per-message reply. + this.grokInitialResponsePending.add(groupId) + try { + await this.sendToGroup(groupId, grokInvitingMessage) + + let member: T.GroupMember + try { + member = await this.withMainProfile(() => + this.chat.apiAddMember(groupId, this.config.grokContactId!, T.GroupMemberRole.Member) + ) + } catch (err: unknown) { + const chatErr = err as {chatError?: {errorType?: {type?: string}}} + if (chatErr?.chatError?.errorType?.type === "groupDuplicateMember") { + // Grok already in group (e.g. customer sent /grok again before join completed) — + // the in-flight activation will handle the outcome, just return silently + return + } + logError(`Failed to invite Grok to group ${groupId}`, err) + await revertStateOnFail() + await this.sendToGroup(groupId, grokUnavailableMessage) + if (opts.sendQueueOnFail) await this.sendToGroup(groupId, queueMessage(this.config.timezone, this.grokEnabled)) + this.cards.scheduleUpdate(groupId) + return + } + + this.pendingGrokJoins.set(member.memberId, groupId) + + // Drain buffered invitation that arrived during the apiAddMember await + const buffered = this.bufferedGrokInvitations.get(member.memberId) + if (buffered) { + this.bufferedGrokInvitations.delete(member.memberId) + this.pendingGrokJoins.delete(member.memberId) + await this.processGrokInvitation(buffered, groupId) + } + + const joined = await this.waitForGrokJoin(groupId, 120_000) + if (!joined) { + this.pendingGrokJoins.delete(member.memberId) + try { + await this.withMainProfile(() => + this.chat.apiRemoveMembers(groupId, [member.groupMemberId]) + ) + } catch {} + this.cleanupGrokMaps(groupId) + await revertStateOnFail() + await this.sendToGroup(groupId, grokUnavailableMessage) + if (opts.sendQueueOnFail) await this.sendToGroup(groupId, queueMessage(this.config.timezone, this.grokEnabled)) + this.cards.scheduleUpdate(groupId) + return + } + + await this.sendToGroup(groupId, grokActivatedMessage) + + // Grok joined — send initial response based on customer's accumulated messages + try { + const grokLocalGId = this.grokGroupMap.get(groupId) + if (grokLocalGId === undefined) { + await this.sendToGroup(groupId, grokUnavailableMessage) + return + } + + // Read history from Grok's own view — only customer messages. + // The previous `grokBc && ...` short-circuit let bot and team + // messages through when Grok's view had no businessChat; require + // grokBc.customerId to be present and match strictly. + const chat = await this.withGrokProfile(() => + this.chat.apiGetChat(T.ChatType.Group, grokLocalGId, 100) + ) + const grokBc = chat.chatInfo.type === "group" ? chat.chatInfo.groupInfo.businessChat : null + const customerMessages: string[] = [] + for (const ci of chat.chatItems) { + if (ci.chatDir.type !== "groupRcv") continue + if (!grokBc || ci.chatDir.groupMember.memberId !== grokBc.customerId) continue + const t = util.ciContentText(ci)?.trim() + if (t && !util.ciBotCommand(ci)) customerMessages.push(t) + } + + if (customerMessages.length === 0) { + await this.withGrokProfile(() => + this.chat.apiSendTextMessage([T.ChatType.Group, grokLocalGId], grokNoHistoryMessage) + ) + return + } + + const initialMsg = customerMessages.join("\n") + const response = await grokApi.chat([], initialMsg) + + await this.withGrokProfile(() => + this.chat.apiSendTextMessage([T.ChatType.Group, grokLocalGId], response) + ) + } catch (err) { + logError(`Grok initial response failed for group ${groupId}`, err) + await this.sendToGroup(groupId, grokUnavailableMessage) + } + } finally { + this.grokInitialResponsePending.delete(groupId) + } + } + + // --- Team activation --- + + private async activateTeam(groupId: number): Promise { + if (this.config.teamMembers.length === 0) { + await this.sendToGroup(groupId, noTeamMembersMessage(this.grokEnabled)) + return + } + + const data = await this.cards.getRawCustomData(groupId) + const alreadyActivated = data?.state === "TEAM-PENDING" || data?.state === "TEAM" + if (alreadyActivated) { + const {teamMembers} = await this.cards.getGroupComposition(groupId) + if (teamMembers.length > 0) { + await this.sendToGroup(groupId, teamAlreadyInvitedMessage) + return + } + // Team previously activated but all team members have since left — + // re-add silently (no teamAddedMessage). State stays TEAM-PENDING/TEAM. + for (const tm of this.config.teamMembers) { + try { + await this.addOrFindTeamMember(groupId, tm.id) + } catch (err) { + logError(`Failed to add team member ${tm.id} to group ${groupId}`, err) + } + } + return + } + + // First activation — write state BEFORE add loop so concurrent customer + // events observing mid-flight see TEAM-PENDING rather than stale state. + await this.cards.mergeCustomData(groupId, {state: "TEAM-PENDING"}) + + for (const tm of this.config.teamMembers) { + try { + await this.addOrFindTeamMember(groupId, tm.id) + } catch (err) { + logError(`Failed to add team member ${tm.id} to group ${groupId}`, err) + } + } + + const {grokMember} = await this.cards.getGroupComposition(groupId) + await this.sendToGroup(groupId, teamAddedMessage(this.config.timezone, !!grokMember)) + } + + // --- Team group commands --- + + private async processTeamGroupMessage(chatItem: T.ChatItem): Promise { + if (chatItem.chatDir.type !== "groupRcv") return + const senderContactId = chatItem.chatDir.groupMember.memberContactId + if (!senderContactId) return + + const cmd = util.ciBotCommand(chatItem) + if (cmd?.keyword !== "join") return + + const targetGroupId = Number.parseInt(cmd.params, 10) + if (Number.isNaN(targetGroupId) || targetGroupId <= 0) { + await this.sendToGroup(this.config.teamGroup.id, `Error: invalid group id "${cmd.params}"`) + return + } + await this.handleJoinCommand(targetGroupId, senderContactId) + } + + private async handleJoinCommand(targetGroupId: number, senderContactId: number): Promise { + // Validate target is a business group + const targetGroup = await this.withMainProfile(() => getGroupInfo(this.chat, targetGroupId)) + if (!targetGroup?.businessChat) { + await this.sendToGroup(this.config.teamGroup.id, `Error: group ${targetGroupId} is not a business chat`) + return + } + + try { + const member = await this.addOrFindTeamMember(targetGroupId, senderContactId) + if (member) { + log(`Team member ${senderContactId} joined group ${targetGroupId} via /join`) + } + } catch (err) { + logError(`/join failed for group ${targetGroupId}`, err) + await this.sendToGroup(this.config.teamGroup.id, `Error joining group ${targetGroupId}`) + } + } + + // --- Helpers --- + + private async addOrFindTeamMember(groupId: number, teamContactId: number): Promise { + // Pre-check membership: skip apiAddMember entirely if the contact is in + // the group in any non-terminal status. The SimpleX API resends the + // invitation for a member in GSMemInvited, so calling apiAddMember on a + // pending invitee would re-trigger an invite notification. + const members = await this.withMainProfile(() => this.chat.apiListMembers(groupId)) + const existing = members.find(m => m.memberContactId === teamContactId && isInGroup(m)) + if (existing) return existing + const member = await this.withMainProfile(() => + this.chat.apiAddMember(groupId, teamContactId, T.GroupMemberRole.Member) + ) + try { + await this.withMainProfile(() => + this.chat.apiSetMembersRole(groupId, [member.groupMemberId], T.GroupMemberRole.Owner) + ) + } catch { + // Not yet connected — will be promoted in onMemberConnected + } + return member + } + + async sendToGroup(groupId: number, text: string): Promise { + try { + await this.withMainProfile(async () => { + await this.syncGroupCommands(groupId) + await this.chat.apiSendTextMessage([T.ChatType.Group, groupId], text) + }) + } catch (err) { + logError(`Failed to send message to group ${groupId}`, err) + } + } + + private waitForGrokJoin(groupId: number, timeout: number): Promise { + if (this.grokFullyConnected.has(groupId)) return Promise.resolve(true) + return new Promise((resolve) => { + const timer = setTimeout(() => { + this.grokJoinResolvers.delete(groupId) + resolve(false) + }, timeout) + this.grokJoinResolvers.set(groupId, () => { + clearTimeout(timer) + resolve(true) + }) + }) + } + + private async sendTeamMemberDM(member: T.GroupMember, memberContact?: T.Contact): Promise { + const name = member.memberProfile.displayName + const formatted = name.includes(" ") ? `'${name}'` : name + + let contactId = memberContact?.contactId ?? member.memberContactId + if (!contactId) { + // No DM contact yet — create one and send invitation with message + try { + const contact = await this.withMainProfile(() => + this.chat.apiCreateMemberContact(this.config.teamGroup.id, member.groupMemberId) + ) + contactId = contact.contactId as number + log(`Created DM contact ${contactId} for team member ${name}`) + } catch (err) { + logError(`Failed to create member contact for ${name}`, err) + return + } + if (this.sentTeamDMs.has(contactId)) return + const msg = `Added you to be able to invite you to customer chats later, keep this contact. Your contact ID is ${contactId}:${formatted}` + try { + await this.withMainProfile(() => + this.chat.apiSendMemberContactInvitation(contactId!, msg) + ) + this.sentTeamDMs.add(contactId) + this.pendingTeamDMs.delete(contactId) + log(`Sent DM invitation to team member ${contactId}:${name}`) + } catch { + this.pendingTeamDMs.set(contactId, msg) + } + return + } + // Contact already exists — send via normal DM + if (this.sentTeamDMs.has(contactId)) return + const msg = `Added you to be able to invite you to customer chats later, keep this contact. Your contact ID is ${contactId}:${formatted}` + try { + await this.withMainProfile(() => + this.chat.apiSendTextMessage([T.ChatType.Direct, contactId], msg) + ) + this.sentTeamDMs.add(contactId) + this.pendingTeamDMs.delete(contactId) + log(`Sent DM to team member ${contactId}:${name}`) + } catch { + this.pendingTeamDMs.set(contactId, msg) + } + } + + private cleanupGrokMaps(groupId: number): void { + const grokLocalGId = this.grokGroupMap.get(groupId) + this.grokFullyConnected.delete(groupId) + this.grokInitialResponsePending.delete(groupId) + if (grokLocalGId === undefined) return + this.grokGroupMap.delete(groupId) + this.reverseGrokMap.delete(grokLocalGId) + } +} diff --git a/apps/simplex-support-bot/src/cards.ts b/apps/simplex-support-bot/src/cards.ts new file mode 100644 index 0000000000..feea986551 --- /dev/null +++ b/apps/simplex-support-bot/src/cards.ts @@ -0,0 +1,479 @@ +import {T} from "@simplex-chat/types" +import {api, util} from "simplex-chat" +import {Mutex} from "async-mutex" +import {Config} from "./config.js" +import {profileMutex, log, logError, getGroupInfo} from "./util.js" + +// State derivation types +export type ConversationState = "WELCOME" | "QUEUE" | "GROK" | "TEAM-PENDING" | "TEAM" + +function isConversationState(x: unknown): x is ConversationState { + return x === "WELCOME" || x === "QUEUE" || x === "GROK" || x === "TEAM-PENDING" || x === "TEAM" +} + +export interface GroupComposition { + grokMember: T.GroupMember | undefined + teamMembers: T.GroupMember[] +} + +interface CardData { + state?: ConversationState + cardItemId?: number + complete?: boolean +} + +function isActiveMember(m: T.GroupMember): boolean { + return m.memberStatus === T.GroupMemberStatus.Connected + || m.memberStatus === T.GroupMemberStatus.Complete + || m.memberStatus === T.GroupMemberStatus.Announced +} + +// Prevent ! from triggering SimpleX markdown styled text (color/small). +// The parser treats !N as color markup (N: 1-6, r, g, b, y, c, m, -) +// and closes at the next !. No escape mechanism exists in the parser, +// so we insert a zero-width space to break the trigger pattern. +function escapeStyledMarkdown(text: string): string { + return text.replace(/!([1-6rgbycm-])/g, "!\u200B$1") +} + +// Truncate a single message to ~maxChars, appending [truncated] if needed +function truncateMsg(text: string, maxChars: number): string { + if (text.length <= maxChars) return text + return text.slice(0, maxChars) + "… [truncated]" +} + +// Describe non-text content types +function contentTypeLabel(ci: T.ChatItem): string | null { + const content = ci.content as T.CIContent + if (content.type !== "rcvMsgContent" && content.type !== "sndMsgContent") return null + const mc = content.msgContent + switch (mc.type) { + case "image": return "[image]" + case "video": return "[video]" + case "voice": return "[voice]" + case "file": return "[file]" + default: return null + } +} + +export class CardManager { + private pendingUpdates = new Set() + private flushInterval: NodeJS.Timeout + // Outer lock; profileMutex (via withMainProfile) is the inner lock. + private customDataMutexes = new Map() + + constructor( + private chat: api.ChatApi, + private config: Config, + private mainUserId: number, + flushIntervalMs = 300 * 1000, + ) { + this.flushInterval = setInterval(() => this.flush(), flushIntervalMs) + this.flushInterval.unref() + } + + private async withMainProfile(fn: () => Promise): Promise { + return profileMutex.runExclusive(async () => { + await this.chat.apiSetActiveUser(this.mainUserId) + return fn() + }) + } + + private getCustomDataMutex(groupId: number): Mutex { + let m = this.customDataMutexes.get(groupId) + if (!m) { + m = new Mutex() + this.customDataMutexes.set(groupId, m) + } + return m + } + + scheduleUpdate(groupId: number): void { + this.pendingUpdates.add(groupId) + } + + async createCard(groupId: number, groupInfo: T.GroupInfo): Promise { + const {text} = await this.composeCard(groupId, groupInfo) + const chatRef: T.ChatRef = {chatType: T.ChatType.Group, chatId: this.config.teamGroup.id} + const items = await this.withMainProfile(() => + this.chat.apiSendMessages(chatRef, [ + {msgContent: {type: "text", text}, mentions: {}}, + ]) + ) + await this.mergeCustomData(groupId, {cardItemId: items[0].chatItem.meta.itemId}) + } + + async flush(): Promise { + const groups = [...this.pendingUpdates] + this.pendingUpdates.clear() + for (const groupId of groups) { + try { + await this.flushOne(groupId) + } catch (err) { + logError(`Card flush failed for group ${groupId}`, err) + } + } + } + + // Dispatches to create-path when cardItemId is absent so a failed createCard retries. + private async flushOne(groupId: number): Promise { + const groupInfo = await this.withMainProfile(() => getGroupInfo(this.chat, groupId)) + if (!groupInfo) return + const data = groupInfo.customData as Record | undefined + if (typeof data?.cardItemId === "number") { + await this.updateCard(groupId) + } else { + await this.createCard(groupId, groupInfo) + } + } + + async refreshAllCards(): Promise { + // Scan the most recently active 1000 chats. Active cards live on + // recently-active customer chats by definition — a card stays open + // while the conversation is in flight. If the bot has been offline + // long enough that an active card has fallen outside this window, the + // card refreshes lazily on the next customer message (which moves the + // chat back into the recent window). + const chats = await this.withMainProfile(() => + this.chat.apiGetChats(this.mainUserId, {type: "last", count: 1000}) + ) + const activeCards: {groupId: number; cardItemId: number}[] = [] + for (const c of chats) { + if (c.chatInfo.type !== "group") continue + const groupInfo = c.chatInfo.groupInfo + const customData = groupInfo.customData as Record | undefined + if (customData && typeof customData.cardItemId === "number" && !customData.complete) { + activeCards.push({groupId: groupInfo.groupId, cardItemId: customData.cardItemId}) + } + } + if (activeCards.length === 0) return + + // Sort ascending by cardItemId — higher ID = more recently updated card. + // Oldest-updated cards refresh first; newest-updated refresh last, + // so the most recent cards end up at the bottom of the team group. + activeCards.sort((a, b) => a.cardItemId - b.cardItemId) + + log(`Startup: refreshing ${activeCards.length} card(s)`) + + for (const {groupId} of activeCards) { + try { + await this.updateCard(groupId) + } catch (err) { + logError(`Startup card refresh failed for group ${groupId}`, err) + } + } + } + + destroy(): void { + clearInterval(this.flushInterval) + } + + // --- State derivation --- + + async getGroupComposition(groupId: number): Promise { + const members = await this.withMainProfile(() => this.chat.apiListMembers(groupId)) + return { + grokMember: members.find(m => + this.config.grokContactId !== null + && m.memberContactId === this.config.grokContactId + && isActiveMember(m)), + teamMembers: members.filter(m => + this.config.teamMembers.some(tm => tm.id === m.memberContactId) + && isActiveMember(m)), + } + } + + async deriveState(groupId: number): Promise { + const data = await this.getRawCustomData(groupId) + return data?.state ?? "WELCOME" + } + + async getLastCustomerMessageTime(groupId: number, customerId: string): Promise { + const chat = await this.getChat(groupId, 20) + for (let i = chat.chatItems.length - 1; i >= 0; i--) { + const ci = chat.chatItems[i] + if (ci.chatDir.type === "groupRcv" && ci.chatDir.groupMember.memberId === customerId) { + return new Date(ci.meta.createdAt).getTime() + } + } + return undefined + } + + async getLastTeamOrGrokMessageTime(groupId: number): Promise { + const chat = await this.getChat(groupId, 20) + for (let i = chat.chatItems.length - 1; i >= 0; i--) { + const ci = chat.chatItems[i] + if (ci.chatDir.type === "groupRcv") { + const contactId = ci.chatDir.groupMember.memberContactId + const isTeam = this.config.teamMembers.some(tm => tm.id === contactId) + const isGrok = this.config.grokContactId !== null && contactId === this.config.grokContactId + if (isTeam || isGrok) return new Date(ci.meta.createdAt).getTime() + } + if (ci.chatDir.type === "groupSnd") { + // Bot's own messages don't count + } + } + return undefined + } + + // --- Custom data --- + + async getRawCustomData(groupId: number): Promise | null> { + const group = await this.withMainProfile(() => getGroupInfo(this.chat, groupId)) + if (!group?.customData) return null + const data = group.customData as Record + const result: Partial = {} + if (isConversationState(data.state)) result.state = data.state + if (typeof data.cardItemId === "number") result.cardItemId = data.cardItemId + if (data.complete === true) result.complete = true + return result + } + + async mergeCustomData(groupId: number, patch: Partial): Promise { + return this.getCustomDataMutex(groupId).runExclusive(async () => { + const current = (await this.getRawCustomData(groupId)) ?? {} + const merged: Partial = {...current, ...patch} + for (const key of Object.keys(merged) as (keyof CardData)[]) { + if (merged[key] === undefined) delete merged[key] + } + await this.withMainProfile(() => this.chat.apiSetGroupCustomData(groupId, merged)) + }) + } + + async clearCustomData(groupId: number): Promise { + return this.getCustomDataMutex(groupId).runExclusive(() => + this.withMainProfile(() => this.chat.apiSetGroupCustomData(groupId)) + ) + } + + // --- Chat history access --- + + async getChat(groupId: number, count: number): Promise { + return this.withMainProfile(() => this.chat.apiGetChat(T.ChatType.Group, groupId, count)) + } + + // --- Internal --- + + private async updateCard(groupId: number): Promise { + const groupInfo = await this.withMainProfile(() => getGroupInfo(this.chat, groupId)) + if (!groupInfo) return + + const customData = groupInfo.customData as Record | undefined + const cardItemId = customData?.cardItemId + if (typeof cardItemId !== "number") return + + try { + await this.withMainProfile(() => + this.chat.apiDeleteChatItems( + T.ChatType.Group, this.config.teamGroup.id, [cardItemId], T.CIDeleteMode.Broadcast + ) + ) + } catch { + // card may already be deleted + } + + const {text, complete} = await this.composeCard(groupId, groupInfo) + const chatRef: T.ChatRef = {chatType: T.ChatType.Group, chatId: this.config.teamGroup.id} + const items = await this.withMainProfile(() => + this.chat.apiSendMessages(chatRef, [ + {msgContent: {type: "text", text}, mentions: {}}, + ]) + ) + const patch: Partial = { + cardItemId: items[0].chatItem.meta.itemId, + complete: complete ? true : undefined, + } + await this.mergeCustomData(groupId, patch) + } + + private async composeCard(groupId: number, groupInfo: T.GroupInfo): Promise<{text: string, complete: boolean}> { + const rawName = groupInfo.groupProfile.displayName || `group-${groupId}` + const customerName = rawName.replace(/\n+/g, " ") + const bc = groupInfo.businessChat + const customerId = bc?.customerId + + const state = await this.deriveState(groupId) + const {teamMembers} = await this.getGroupComposition(groupId) + + const icon = await this.computeIcon(groupId, state, customerId ?? undefined) + const waitStr = await this.computeWaitTime(groupId, state, customerId ?? undefined) + + const chat = await this.getChat(groupId, 100) + const msgCount = chat.chatItems.filter((ci: T.ChatItem) => ci.chatDir.type !== "groupSnd").length + + const stateLabel = this.stateLabel(state) + + const agentNames = teamMembers.map(m => m.memberProfile.displayName) + const agentStr = agentNames.length > 0 ? ` · ${agentNames.join(", ")}` : "" + + const preview = this.buildPreview(chat.chatItems, customerName, customerId) + + // Final line uses /'join ' quoting so SimpleX clients render the full + // command (including the argument) as a single clickable token. + const joinCmd = `/'join ${groupId}'` + + const line1 = `${icon} *${customerName}* · ${waitStr} · ${msgCount} msgs` + const line2 = `${stateLabel}${agentStr}` + return {text: `${line1}\n${line2}\n${preview}\n${joinCmd}`, complete: icon === "✅"} + } + + private async computeIcon( + groupId: number, state: ConversationState, customerId?: string, + ): Promise { + const now = Date.now() + const completeMs = this.config.completeHours * 3600_000 + + // Check auto-complete: last team/Grok message time vs customer silence + const lastTeamGrokTime = await this.getLastTeamOrGrokMessageTime(groupId) + if (lastTeamGrokTime) { + const lastCustTime = customerId + ? await this.getLastCustomerMessageTime(groupId, customerId) + : undefined + // Auto-complete if team/grok replied and customer hasn't responded since, for completeHours + if (!lastCustTime || lastCustTime < lastTeamGrokTime) { + if (now - lastTeamGrokTime >= completeMs) return "✅" + } + } + + switch (state) { + case "QUEUE": { + const lastCustTime = customerId + ? await this.getLastCustomerMessageTime(groupId, customerId) + : undefined + if (!lastCustTime) return "🟡" + const waitMs = now - lastCustTime + if (waitMs < 5 * 60_000) return "🆕" + if (waitMs < 2 * 3600_000) return "🟡" + return "🔴" + } + case "GROK": + return "🤖" + case "TEAM-PENDING": + return "👋" + case "TEAM": { + // Check if customer follow-up unanswered > 2h + const lastCustTime = customerId + ? await this.getLastCustomerMessageTime(groupId, customerId) + : undefined + if (lastCustTime && lastTeamGrokTime && lastCustTime > lastTeamGrokTime) { + return (now - lastCustTime > 2 * 3600_000) ? "⏰" : "💬" + } + return "💬" + } + default: + return "🟡" + } + } + + private async computeWaitTime( + groupId: number, _state: ConversationState, customerId?: string, + ): Promise { + const now = Date.now() + const completeMs = this.config.completeHours * 3600_000 + + const lastTeamGrokTime = await this.getLastTeamOrGrokMessageTime(groupId) + if (lastTeamGrokTime) { + const lastCustTime = customerId + ? await this.getLastCustomerMessageTime(groupId, customerId) + : undefined + if (!lastCustTime || lastCustTime < lastTeamGrokTime) { + if (now - lastTeamGrokTime >= completeMs) return "done" + } + } + + const lastCustTime = customerId + ? await this.getLastCustomerMessageTime(groupId, customerId) + : undefined + if (!lastCustTime) return "<1m" + return this.formatDuration(now - lastCustTime) + } + + private stateLabel(state: ConversationState): string { + switch (state) { + case "QUEUE": return "Queue" + case "GROK": return "Grok" + case "TEAM-PENDING": return "Team – pending" + case "TEAM": return "Team" + default: return "Queue" + } + } + + private buildPreview(chatItems: T.ChatItem[], customerName: string, customerId?: string): string { + const maxTotal = 500 + const maxPer = 200 + + // Collect entries in chronological order (oldest first) + const entries: {senderId: string; name: string; text: string}[] = [] + for (const ci of chatItems) { + if (ci.chatDir.type === "groupSnd") continue + + let text = (util.ciContentText(ci)?.trim() || "").replace(/\n+/g, " ") + const mediaLabel = contentTypeLabel(ci) + if (mediaLabel && !text) text = mediaLabel + else if (mediaLabel) text = `${mediaLabel} ${text}` + if (!text) continue + + let senderId = "" + let name = "" + if (ci.chatDir.type === "groupRcv") { + const member = ci.chatDir.groupMember + const contactId = member.memberContactId + senderId = member.memberId + if (this.config.grokContactId !== null && contactId === this.config.grokContactId) { + name = "Grok" + } else if (customerId && member.memberId === customerId) { + name = customerName + } else { + name = member.memberProfile.displayName + } + } + + entries.push({senderId, name, text: truncateMsg(text, maxPer)}) + } + + // Compute prefixed lines in chronological order (sender prefix on first msg of each run) + const lines: {line: string; senderId: string; name: string}[] = [] + let lastSenderId = "" + for (const entry of entries) { + let line = entry.text + if (entry.senderId !== lastSenderId && entry.name) { + line = `${entry.name}: ${line}` + lastSenderId = entry.senderId + } + lines.push({line, senderId: entry.senderId, name: entry.name}) + } + + // Take from the end (newest) until maxTotal exceeded — oldest messages are truncated + const selected: string[] = [] + let totalLen = 0 + let firstSelectedIdx = lines.length + for (let i = lines.length - 1; i >= 0; i--) { + if (totalLen + lines[i].line.length > maxTotal && selected.length > 0) { + break + } + selected.push(lines[i].line) + totalLen += lines[i].line.length + firstSelectedIdx = i + } + selected.reverse() + + // If truncation happened, ensure the first visible message has a sender prefix + if (firstSelectedIdx > 0 && selected.length > 0) { + const first = lines[firstSelectedIdx] + if (first.name && !selected[0].startsWith(`${first.name}: `)) { + selected[0] = `${first.name}: ${selected[0]}` + } + selected.unshift("[truncated]") + } + + const preview = selected.map(escapeStyledMarkdown).join(" !3 /! ") + return preview ? `"${preview}"` : '""' + } + + private formatDuration(ms: number): string { + if (ms < 60_000) return "<1m" + if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m` + if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h` + return `${Math.floor(ms / 86_400_000)}d` + } +} diff --git a/apps/simplex-support-bot/src/config.ts b/apps/simplex-support-bot/src/config.ts new file mode 100644 index 0000000000..8fbd006aef --- /dev/null +++ b/apps/simplex-support-bot/src/config.ts @@ -0,0 +1,144 @@ +import {Command} from "commander" +import {api} from "simplex-chat" + +export interface IdName { + id: number + name: string +} + +export type Backend = "sqlite" | "postgres" + +export interface Config { + stateFile: string // local path to the bot's state JSON + db: api.DbConfig // passed to ChatApi.init / bot.run + teamGroup: IdName // name from CLI, id resolved at startup from state file + teamMembers: IdName[] // optional, empty if not provided + grokContactId: number | null // resolved at startup + timezone: string + completeHours: number + cardFlushSeconds: number + contextFile: string | null + grokApiKey: string | null +} + +// Mirrors packages/simplex-chat-nodejs/src/download-libs.js so runtime detection +// matches what was used at install time. Works whether the user installed via +// SIMPLEX_BACKEND env var, .npmrc (→ npm_config_simplex_backend), or the +// --simplex_backend=postgres CLI flag (also surfaced as npm_config_*). +export function detectBackend(): Backend { + const raw = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || "sqlite").toLowerCase() + if (raw !== "sqlite" && raw !== "postgres") { + throw new Error(`Invalid SIMPLEX_BACKEND: "${raw}". Must be "sqlite" or "postgres".`) + } + return raw +} + +export function parseIdName(s: string): IdName { + const i = s.indexOf(":") + if (i < 1) throw new Error(`Invalid ID:name format: "${s}"`) + const id = parseInt(s.slice(0, i), 10) + if (isNaN(id)) throw new Error(`Invalid ID:name format (non-numeric ID): "${s}"`) + return {id, name: s.slice(i + 1)} +} + +function parseNonNegativeInt(flag: string) { + return (raw: string): number => { + const n = parseInt(raw, 10) + if (!Number.isFinite(n) || n < 0) { + throw new Error(`${flag} must be a non-negative integer, got "${raw}"`) + } + return n + } +} + +function buildCommand(): Command { + return new Command() + .name("simplex-chat-support-bot") + .description("business-address triage bot") + .requiredOption("--team-group ", "team group display name") + .option("--state-file ", "state JSON path", "./data/state.json") + .option("--sqlite-file-prefix ", "SQLite DB file prefix", "./data/simplex") + .option("--sqlite-key ", "SQLCipher encryption key (default: unencrypted)") + .option("--pg-conn ", "PostgreSQL connection string (required for postgres)") + .option("--pg-schema ", "PostgreSQL schema prefix (default: simplex_v1)") + .option("-a, --auto-add-team-members ", "comma-separated ID:name pairs (e.g. 1:Alice,2:Bob)") + .option("--timezone ", "IANA timezone for weekend detection", "UTC") + .option("--complete-hours ", "auto-complete chats after N hours idle (0 disables)", parseNonNegativeInt("--complete-hours"), 3) + .option("--card-flush-seconds ", "debounce card state writes", parseNonNegativeInt("--card-flush-seconds"), 300) + .option("--context-file ", "text file with Grok system context (required if GROK_API_KEY set)") + .addHelpText("after", "\nEnvironment:\n GROK_API_KEY xAI API key — enables Grok replies\n SIMPLEX_BACKEND sqlite | postgres — alternative to .npmrc for backend selection\n") +} + +interface RawOpts { + teamGroup: string + stateFile: string + sqliteFilePrefix: string + sqliteKey?: string + pgConn?: string + pgSchema?: string + autoAddTeamMembers?: string + timezone: string + completeHours: number + cardFlushSeconds: number + contextFile?: string +} + +export function parseConfig(args: string[]): Config { + const cmd = buildCommand().exitOverride() + try { + cmd.parse(args, {from: "user"}) + } catch (err) { + const code = (err as {code?: string}).code + if (code === "commander.helpDisplayed" || code === "commander.version") process.exit(0) + throw err + } + const opts = cmd.opts() + + const grokApiKey = process.env.GROK_API_KEY || null + + const backend = detectBackend() + let db: api.DbConfig + if (backend === "sqlite") { + db = opts.sqliteKey + ? {type: "sqlite", filePrefix: opts.sqliteFilePrefix, encryptionKey: opts.sqliteKey} + : {type: "sqlite", filePrefix: opts.sqliteFilePrefix} + } else { + if (!opts.pgConn) { + throw new Error("--pg-conn is required when backend is postgres (PostgreSQL connection string)") + } + db = opts.pgSchema + ? {type: "postgres", connectionString: opts.pgConn, schemaPrefix: opts.pgSchema} + : {type: "postgres", connectionString: opts.pgConn} + } + + const teamGroup: IdName = {id: 0, name: opts.teamGroup} + + const teamMembersRaw = opts.autoAddTeamMembers ?? "" + const teamMembers = teamMembersRaw + ? teamMembersRaw.split(",").map(parseIdName) + : [] + + try { + new Intl.DateTimeFormat("en-US", {timeZone: opts.timezone, weekday: "short"}) + } catch (err) { + throw new Error(`--timezone "${opts.timezone}" is not a valid IANA time zone: ${(err as Error).message}`) + } + + const contextFile = opts.contextFile ?? null + if (grokApiKey && !contextFile) { + throw new Error("GROK_API_KEY is set but --context-file is not provided. Grok requires a context file.") + } + + return { + stateFile: opts.stateFile, + db, + teamGroup, + teamMembers, + grokContactId: null, + timezone: opts.timezone, + completeHours: opts.completeHours, + cardFlushSeconds: opts.cardFlushSeconds, + contextFile, + grokApiKey, + } +} diff --git a/apps/simplex-support-bot/src/grok.ts b/apps/simplex-support-bot/src/grok.ts new file mode 100644 index 0000000000..b03108439a --- /dev/null +++ b/apps/simplex-support-bot/src/grok.ts @@ -0,0 +1,55 @@ +import {log, logError} from "./util.js" + +export interface GrokMessage { + role: "system" | "user" | "assistant" + content: string +} + +export class GrokApiClient { + private readonly apiKey: string + private readonly systemPrompt: string + + constructor(apiKey: string, systemPrompt: string) { + this.apiKey = apiKey + this.systemPrompt = systemPrompt + } + + async chatRaw(messages: GrokMessage[]): Promise { + const response = await fetch("https://api.x.ai/v1/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + model: "grok-3-mini", + messages, + temperature: 0.3, + max_tokens: 1024, + }), + signal: AbortSignal.timeout(60_000), + }) + + if (!response.ok) { + const body = await response.text() + logError(`Grok API HTTP ${response.status}`, body) + throw new Error(`Grok API error: HTTP ${response.status}`) + } + + const data = await response.json() as {choices: {message: {content: string}}[]} + const content = data.choices?.[0]?.message?.content + if (!content) throw new Error("Grok API returned empty response") + + log(`Grok API response: ${content.length} chars`) + return content + } + + async chat(history: GrokMessage[], userMessage: string): Promise { + log(`Grok API call: ${history.length} history msgs, user msg ${userMessage.length} chars`) + return this.chatRaw([ + {role: "system", content: this.systemPrompt}, + ...history, + {role: "user", content: userMessage}, + ]) + } +} diff --git a/apps/simplex-support-bot/src/index.ts b/apps/simplex-support-bot/src/index.ts new file mode 100644 index 0000000000..6f392e9deb --- /dev/null +++ b/apps/simplex-support-bot/src/index.ts @@ -0,0 +1,369 @@ +import {readFileSync, writeFileSync, existsSync} from "fs" +import {api, bot, util} from "simplex-chat" +import {T} from "@simplex-chat/types" +import {parseConfig} from "./config.js" +import {SupportBot} from "./bot.js" +import {GrokApiClient} from "./grok.js" +import {welcomeMessage} from "./messages.js" +import {profileMutex, log, logError, getGroupInfo, getContact} from "./util.js" + +interface BotState { + teamGroupId?: number + grokContactId?: number + grokUserId?: number +} + +function readState(path: string): BotState { + if (!existsSync(path)) return {} + try { return JSON.parse(readFileSync(path, "utf-8")) } catch { return {} } +} + +function writeState(path: string, state: BotState): void { + writeFileSync(path, JSON.stringify(state), "utf-8") +} + +async function main(): Promise { + const config = parseConfig(process.argv.slice(2)) + // Do not log config.db.connectionString — typically contains credentials. + log("Config parsed", { + stateFile: config.stateFile, + backend: config.db.type, + teamGroup: config.teamGroup, + teamMembers: config.teamMembers, + timezone: config.timezone, + completeHours: config.completeHours, + }) + const grokEnabled = config.grokApiKey !== null + if (!grokEnabled) log("No GROK_API_KEY provided, disabling Grok support") + + const stateFilePath = config.stateFile + const state = readState(stateFilePath) + + // Forward-reference for event handlers during init + let supportBot: SupportBot | undefined + + // On restart, the active user may be Grok (if the previous run was killed + // mid-profile-switch). bot.run() uses apiGetActiveUser() and would then + // operate against the Grok userId as if it were the main user. Restore + // the main user as active before bot.run(). Grok is identified by the + // userId persisted in state.json on first resolution — comparing by + // profile name is fragile to renames. + if (state.grokUserId !== undefined) { + const preChat = await api.ChatApi.init(config.db) + try { + const activeUser = await preChat.apiGetActiveUser() + if (activeUser && activeUser.userId === state.grokUserId) { + const users = await preChat.apiListUsers() + const mainCandidates = users.filter(u => u.user.userId !== state.grokUserId) + if (mainCandidates.length === 0) { + throw new Error( + `DB has only the Grok user (userId=${state.grokUserId}); no main user to restore. ` + + `Likely a corrupted migration or partial restore.` + ) + } + if (mainCandidates.length > 1) { + const names = mainCandidates.map(u => `${u.user.userId}:${u.user.profile.displayName}`).join(", ") + throw new Error( + `Ambiguous DB state: multiple non-Grok users [${names}]. ` + + `Refusing to guess which is main — remove extras manually.` + ) + } + const mainUserInfo = mainCandidates[0] + await preChat.apiSetActiveUser(mainUserInfo.user.userId) + log(`Restored active user to ${mainUserInfo.user.profile.displayName} (userId=${mainUserInfo.user.userId})`) + } + } finally { + await preChat.close() + } + } + + // Profile images (base64-encoded JPEG) + const supportImage = "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCACAAIADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD6pooooAKKKKACiignAyelABRQCGAIIIPIIooAKKKKACikjdZEDxsGU8gqcg0tAk01dBRRRQMKKKKACiiigAooooAK898ZeKftBew058Qj5ZZR/H7D29+9ehVxHjTwt5++/wBMT9996WFR9/8A2h7+3f69e/LnRVZe1+Xa587xNTxtTBNYP/t627Xl+vVr8c/wf4oNkyWWoPm1PCSH/ln7H/Z/lXo6kMAVIIPIIrwTdiuw8GeKjYsljqDk2h4SQ/8ALP2P+z/KvSzDLua9WkteqPmOGeJHQtg8Y/d+zLt5Py7Pp6bel1wXjHxRv32GmyfJ92WZT97/AGV9vU1H4z8ViTfYaZJ+7+7LMp+9/sqfT1NcOGqMvy61qtVeiNeJuJea+Dwb02lJfkv1Z1PhTxI+lSiC5JeyY8jqYz6j29RXp6MHRWU5VhkGuG8F+F8eXqGpx8/eihYdP9ph/IV3VcWZTpSq/u9+p7fCdDG0cHbFP3X8Ke6X+XZdAooorzj6kKKKKACiikYhVJYgAckmgBTxRXzJ8dPi6dUNx4d8LXGNPGY7u8jP+v8AVEP9z1P8XQcddL4E/F7/AI9/Dfiu49I7K+kbr2Ech/QN+B7Gu95dWVH2tvl1scqxdN1OQ+iaKKK4DqOG8b+FPPEmoaYn7770sKj7/wDtD39u/wBevnAas346/F77X9o8N+FLj/R+Y7y+jb/WdjHGf7vYt36DjJPnvgPxibXy9M1aT/R+FhnY/wCr9FY/3fQ9vp0+ty32qpJVvl3sfnPEmS051HiMItftJfmv1PVN1eheCPCvEeo6mmScNDC36M39BXm+6u18EeLTYMljqTk2h4jkP/LL2P8As/yrTMIVnRfsfn3t5Hh8PPB08ZF4xadOyfn/AF6nqNFIrBlDKQQeQR3pa+OP2IKRHV1DIwZT0IORXn/jjxdt8zTtLk+b7s0ynp6qp/maxPB3il9HmFvdFnsHPI6mM+o9vUV6cMqrTo+169F5HzNfinCUcYsM9Y7OXRP/AC7voeuUU2KRZY0kjIZGAZSO4NOrzD6VO+qCkZQylWAKkYIPelooGfMHxz+EZ0Zp/EPheAnTDl7q0jH/AB7eroP7nqP4fp08Lr9EmUMpVgCDwQa+Yfjn8Im0dp/EPhe3LaaSXurOMZNue7oP7nqP4fp09/L8w5rUqr16M8vF4S3vwNb4FfF7/j38N+K7jniOyvpG69hHIT+QY/Q9jVb47fF03RufDfhS4xbjMd7exn/WdjHGf7vYt36DjJPz/RXZ/Z9H23tbfLpfuc/1up7PkE6D0FfRnwK+EOw2/iTxXb/PxJZ2Mi/d7iSQevcL26nnAB8C/hD5Zt/Efiy3xJxJZ2Mq/d7iSQHv3C9up5wB9D1wZhmG9Kk/VnVhMJ9uZwPjvwj9o8zUtKj/AH33poVH3/8AaX39R3+vXzLdX0XXn3j3wd9o8zUtJj/f/emgUff/ANpR6+o7/XrpleZ2tRrPTo/0Z8xxFw5z3xeEWvVd/NfqjL8DeLzp7JYam5NmTiOQ/wDLL2P+z/KtDx14xAD6dpEuT0mnQ9P9lT/M15nu5pd1etLLKMq3tmvl0v3Pm4Z9jIYP6mpad+qXYn3V6D4E8ImXy9S1WP8Ad/ehgYfe9GYenoKj8A+EPOEWp6tH+74aCBh970Zh6eg716ZXl5nmVr0aL9X+iPe4d4cvbF4tecY/q/0QUUUV86ffhRRRQAV82/HX4vfa/tHhvwpcf6NzHeX0bf6zsY4z/d7Fu/QcZJPjr8XvtRuPDfhS4/0fmO8vo2/1nYxxkfw9i3foOMk/P/8AKvdy/L7Wq1V6I8zF4v7EBOn0pa+i/gX8INot/Efiy2+fiSzsZV+76SSA9/RT06nnAGP8dPhGdHa48Q+F4CdMJL3Vogybc93Qf3PUfw/Tp3rH0XV9lf59L9jleFqKn7Q1vgV8Xjm38N+LLnJ4js76VuvYRyE/kGP0PY19E1+dlfRXwJ+L3Nv4b8V3HPEdlfSN17COQn8g34Hsa8/MMv3q0l6o68Ji/sTPomvNfiB412mTS9Hl+blZ7hT09VU+vqaj+InjfYZdK0eX5uVnuFPT1VT6+p/CvMN1dOVZTe1euvRfqz5riDP98LhX6v8ARfqybdS7q9E+HngszeVqmsRfu+Ggt2H3vRmHp6DvVz4heC/tAk1PR4v3/wB6aBR9/wD2lHr6jv8AXr6TzTDqv7C/z6X7Hgx4dxcsJ9aS/wC3etu//AMrwD4zOnMmn6pITZE4jlY5MXsf9n+X0r1pWDKGUgqRkEd6+Zd2K7z4f+NDprR6dqrk2JOI5T/yx9j/ALP8vpXFmuU8961Ba9V3815/mevw/n7o2wuKfu9H28n5fl6bev0UisGUMpBUjII70tfKn3wVHdQRXVtLb3CCSGVCjoejKRgg/hUlFAHx98Z/hbceCrttQ0tXm8PTNhWPLWrHojn09G/A89e7+BXwh8v7P4k8V2/z8SWdjIv3e4kkB79wvbqecAfQc0Mc8TRzRpJG3VXUEH8DT69GeZVZ0vZ9e5yRwcI1Of8AAKRlDKVYAg8EGlorzjrPmD45/CM6O0/iHwvATphJe6tIx/x7+roP7nqP4fp04Hwh4aB2X+pR8feihYdf9ph/IV9EfErx2B52kaLKCeUuLhT09UU/zP4V5Tur7jKaFaVFTxHy728z4LPcxgpujhX6v9F+pPur074c+CDN5Wq6zF+64aC3cfe9GYenoO9eV7q9d+G/joXXlaVrUv8ApHCwXDH/AFnorH+96Hv9eumb/WI4duh8+9vI87IaeFeKX1n5dr+f6HptFFFfBn6ceb/ETwT9pEuqaNH/AKR96eBR/rPVlH971Hf69c34d+CTdmPU9ZiIth80MDj/AFn+0w/u+g7/AE6+tUV6kc2rxw/sE/n1t2PEnkGEnivrTXy6X7/8AAAAABgCiiivLPbCiiigAooooAK8n+Jnj7YZdI0OX5uUuLlD09UU+vqfwFerSossbxuMowKkeoNeBfETwTL4cuDd2QaTSpG4PUwk/wALe3ofwPPX2sjpYepiLVnr0XRv+uh4Wf1cTTw37hadX1S/rdnG7q9U+GngPzxFq2uRfueGt7Zx9/0dh6eg79TTPhj4B87ytY1yL91w9vbOPv8Ao7D09B36mvYK9POc4tfD4d+r/RHlZJkV7YnEr0X6v/I8U+JPgZtKaTVNIjLaeTuliXkwH1H+z/L6V52GxX1c6q6lWAKkYIIyDXiXxL8CNpLSapo8ZbTyd0sK9YPcf7P8vpV5PnHtLYfEPXo+/k/P8/XfLO8i9nfE4ZadV2815fl6bb/w18eC68rSdbl/0j7sFw5/1norH+96Hv8AXr6fXjXwy8Bm9MWr61ERajDQW7D/AFvozD+76Dv9OvsteLnMcPHENYf59r+R72RyxMsMnifl3t5/oFFFFeSeyFFFFABRRRQAUUUUAFMmijmjaOZFkjYYZXGQR7in0UJ2Bq+4UUUUAFIyh1KsAVIwQRwaWigAAAAAGAKKKKACiiigAooooA//2Q==" + const grokImage = "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gKgSUNDX1BST0ZJTEUAAQEAAAKQbGNtcwQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwQVBQTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWxjbXMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtkZXNjAAABCAAAADhjcHJ0AAABQAAAAE53dHB0AAABkAAAABRjaGFkAAABpAAAACxyWFlaAAAB0AAAABRiWFlaAAAB5AAAABRnWFlaAAAB+AAAABRyVFJDAAACDAAAACBnVFJDAAACLAAAACBiVFJDAAACTAAAACBjaHJtAAACbAAAACRtbHVjAAAAAAAAAAEAAAAMZW5VUwAAABwAAAAcAHMAUgBHAEIAIABiAHUAaQBsAHQALQBpAG4AAG1sdWMAAAAAAAAAAQAAAAxlblVTAAAAMgAAABwATgBvACAAYwBvAHAAeQByAGkAZwBoAHQALAAgAHUAcwBlACAAZgByAGUAZQBsAHkAAAAAWFlaIAAAAAAAAPbWAAEAAAAA0y1zZjMyAAAAAAABDEoAAAXj///zKgAAB5sAAP2H///7ov///aMAAAPYAADAlFhZWiAAAAAAAABvlAAAOO4AAAOQWFlaIAAAAAAAACSdAAAPgwAAtr5YWVogAAAAAAAAYqUAALeQAAAY3nBhcmEAAAAAAAMAAAACZmYAAPKnAAANWQAAE9AAAApbcGFyYQAAAAAAAwAAAAJmZgAA8qcAAA1ZAAAT0AAACltwYXJhAAAAAAADAAAAAmZmAADypwAADVkAABPQAAAKW2Nocm0AAAAAAAMAAAAAo9cAAFR7AABMzQAAmZoAACZmAAAPXP/bAEMABQMEBAQDBQQEBAUFBQYHDAgHBwcHDwsLCQwRDxISEQ8RERMWHBcTFBoVEREYIRgaHR0fHx8TFyIkIh4kHB4fHv/bAEMBBQUFBwYHDggIDh4UERQeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHv/AABEIAIAAgAMBIgACEQEDEQH/xAAdAAAABgMBAAAAAAAAAAAAAAAAAQQHCAkCAwUG/8QAOxAAAQIEBAQDBgUCBgMAAAAAAQIDAAQFEQYHITEIEkFRE2FxFCJCgZGhFTJSgrEJIzNDYnKS4VOy8P/EABYBAQEBAAAAAAAAAAAAAAAAAAABAv/EABsRAQEBAAMBAQAAAAAAAAAAAAABEQISIRMx/9oADAMBAAIRAxEAPwCINoAv3gCD3BjTIhpprA3gdTB2F9NoAE9oGm194IWMAQB/WB3gD7QNIAbwBtvA2PnBjtAF08oHe/ygDUwfTf6QA6wRsNYMX84NKSYDEHXYQY1jc5Kvty7Uw42pDTpUG1H4+U2JHcA6X76RpSn7bwBQB1gesADXvAD0gAaX0gDY2gWgCEGBcHSM2m1LNrR7LLrLvE+NqsKbhukTE++LFwpHK20D8S1n3Uj1OvS8B45DK1dI3JknSNEk/KJoZfcH0gyy3MY1xA666RdUpTEhKU+RdWCT8kiHbpHD3lHTmUtpwhLTShu5NvOPKPrdVvtE1cVrGReA/IfpGlcstF7pMWeTOReUswjkXgWkpHdtKmz9UkGPBYx4Tcv6o04uhTdSoUwfyAOe0Mj9q/e+ihDTFfikLSSCLQBa58+gh684uH7G+X8u7UH5NFVpCNTPyIKktju4gjmR66p84Z1EupblgmKjUy2pZ01hy8s8u2qjQ6hjjFSn5HBtII9odQeV2fe+GVl7/Go2BVskEnfb1vDVkXPZj1NNRqQeksMyrlpmYAsqYUN2mj3/AFK+H1hfxfYzkZrEUtl7hppqTwzhYezty7As2qZtZZ035fyi/XnPWIpjsW1h+t1l6oOsMSyFAIYlWE2almU6IabHRKRp3JuTckmOMBvGx1dyTbaMQSTfYHvFRqG0DWDtpA9IAvWM2Wys2jG149blphSoYuxbTcPUxF5uffS0gkXCBupZ8kpBUfIQHvuHHJepZmVwqWpySoUmoe2zoTrffwm76FZHySNT0BsBwVhSg4PoLFEw9TmZGTZGiUD3lq6qWrdSj1J1jXl9hSk4KwlIYcozIblZRvl5iPedWdVOK7qUbk/9Q2PExnhL5cyP4LRCzM4lmW+YBfvIk0G9nFjqo/Cn5nTfLRyscY6wngqRE3iauSlOSr/DQtV3HP8AagXUr5CGSxDxd4QlHy3RsPVapJH+Y84iXSfQe8r6gRC7FmKarXqs/U6tPzE9Ovqu4++sqWryv0HkNB0jgLmnCd4uJqbklxi0lbwTN4Km22ydVM1BCyB6FI/mPZJ4psrjQXKh49VTNIICZBUmfGWSOir8lvMqiu9Mw4NeYwol3XlmwJ1hhqRuanE7jTFCXqfh0Iw3TnAUnwD4kytJ6KcIsn9oHqY5PDdkfPZjVb8TqSXZTDUs5aZmBoqYUN2mj3/Ur4fWC4ackqjmNVBUakHpTDMq5aYmALKmFDdpo9/1K+H1ifFEpdPotJlqVSpRmTkpVsNsMtJslCR0H/2u8BwcUTNMy+yvqU1S5RiSkqLTXFyzDSLITyJPIkDzVb1vFXdemnpibdefcLjziytxZ3Uom5J9STFknFEVjIPFhRv7IkfLxUX+0Vp1TV5R13hAh6awBB2Fu5gCxiow+UEN9TB9ILQ9TAZsJ5nABEwv6fuEW3alXMYzDXMZRCZGVKhstfvOEefKED9xiIMgLvA+cWJ8E1PTJZEST4A5p2dmX1H0X4Y+yBEWHQzAxJKYQwVVsSzo5mafLKd5P1q2Sj9yiB84q+x7iSpYhxDP1mqTBfnZ19Tzyyd1HoOwAsAOgAibXHfWVyOVUhS21lJqNSQF2P5kNoUu3/Ll+kQEnlFTh1trCBOpRUreCFzA0vGxlsrVFRlLsqcVoIffhmyOn8xqt+IVAOyeGZRwCZmQLKfUP8lo9+6tkjzsI0cNGSU9mPVfxGoeJI4YknLTc2PdU8oalponrb8ytkjzsIezOLiFw7gajpwRlSxJKXJN+zicaSFS0oBpytDZ1d/iPu31PMbxFOxmJmPgXJvDMrSkNMpeaZCJCjydgvkGxP6E33Urc33MYZC5wyGaDFRaFONMqEhyKWx43ihbargLSbA6EWII007xXRX8QVGr1R+oVCcfnJyYWVvPvLKlrV3JO8PfwRVp6TzrkZXnIbqMpMSyx3sjxB92/vATMzmpBruVGKKUnVb9Lf8ADFt1hBUn7gRVxVB/cJI31EW5OJSttSFpCkqFiD1BiqPH0imn4lqkgkWTLTjzIHklxSR/EIPNA2Gt4I6bWN/tAG+kFtFQUA2gDaAOusAokD/eGoixrgym0TOQdJbQdZaYmWVeR8ZSv4UIrhl1crmneJt/0/sToeolfwk64A4y8ifl090qAQ59ClH/ACiLHT4/pB17L+gVBAJblqkptZ7c7Srf+kQSmxZ0xaTnrg046yurGH2kpM4toPSZV0fbPMj625f3RWPXZF6Vm3WnmlNOtqKVoULFKgbEEdCDpCDlNpufKHSyay6k68y9inGFSFBwTTnOWcn1aLmXN/ZpcbrcPWwPKPOwhvaEKaidS9Vg+5Kt+8phhXKt/sgK1CAeqrEgbAm0dfF+MKriVyWTOKal5GRb8Gn0+VSUSsk3+ltF9L7lRupR1USYqHSzgz2ma7R0YMwRJHDGC5VsMMybJ5XphA/8pB0B35AdSTzFRhjn5lTht0jUtalExjy7xFBIJVoYf3gqkXZvPOjLSklEozMPuEdAGlJH3WIYiTaUt0C0TZ4CsDOyNKqmN5xko9sAkpEkfmbSq7ix5FQSn9pgRKTpFVGZs0icxlW5ps3S/UZhxNuxdUR/MWW5vYkawllnX6+4vlXKyS/B11Lqhytj5qUmKtas4Vum6iT1N94Qc/vbeBt5QQMH6jSKgbwAL6wYFxeM0pBMASLhUPtwc1GYkM8aCGlK5ZrxZZ1I+JCmlHX5pSflDIMNErAF4k7wMYOfqWYLuKHGiJKisKCVkaKmHU8qU/JJUfp3gqb3w6xBHjepuCmMw/aMPTgNbfuqsSrSQWm19FlXRxXxJ9CbE6uxxH8QjVITM4VwLNpcnxducqbZBTL9Cho7FfdWyelztFGh0Ku4vrPsVIp07VZ51XMpDKC4o3OqlHprupR+cJDXiCyq5sD6wXgq1uDEucAcI9TnZMzOMK0ikrWj+3KyaA84k9OdR93Tsm/qIS17hExQw6s0au0ifav7vjhbC/mLKH3i+CKCWVfpjexKLWqwB17RJaQ4T8fuupS/MUKWRfVaptavsEQ6OX3ClhulvtzeLKq7WlpIPsrCCwxfso3K1D5pieCPPD3kvWMw662stuytCl3B7bPFNhbq23+pZ+idz0BsHodLkKJRpSk0yWblZKTZSyw0gWCEJFgP++sZ0qnSFJpzMhTZRiTlGEcjTLKAhCE9gBoIZ7iPzukMB0t+iUN9qaxM8jlABCkyQI/Ov/V+lHzOm8/Q1HHPmS1NzjGAKZMBbMksTFSUg3BeseRr9oJUfMjtEQ5lXOsnXeOxXJ5+em3pmYeceedWVuOOKupaibkk9STrHHKd4uDRbvpBRmU77aQQGveCM0J1jey2VaRggXjuYWos3Wqo1T5INBxV1LceXyNMtpF1OOKOiUJGpJ+5sDYOvlpgyr4zxLL0SjtJLzl1uvOaNS7Q/M64rolI+uw1MPLmDmnS8K4MRlflXNOIpTAUmpVpPuu1B0/4hQRsknQq6iwHui5b+v4skKVhp3BWCFuJpT1jVKmpBQ/WHB3G7cuPhb3O6tTYeHJKusWQ0TzqnDoIczJHN3E2W77jNMUxN0uYc55iQmE+4s7cyVD3kqt11HcGG0Si+to2oBTtGsY1PfA3EVl/X2EIqU07QJsgczc6Lt38nU6W9eWHRpldotTbDlNq8hOIOymJlDgP0MVgMzDiNlfO8KWZ9aNUmx7jQxn5tTktAfmpZhBW/MMtJHVawkfePG4rzay+w02v8QxPIuPJF/AlV+O6fLlRe3ztFerlTfcFlrUr/cSf5hM5NrUCL2HlDod0jc2OJ2q1Jl6m4LlnKRLKukzrpBmVD/SBdLfrqfSIzVWcenHnHnnVuuOKKlrWoqUpROpJOpJ7mMnFKXurbaNC081o1OOJ2c5xF7nWE6kWBjprb3hM63oTa0TFlc1aI1KBF4WuotftCVabEiM2K2N6bWjqSc0+1KuyrbikNPlJdSk25+U3APcA622vr0Ecxu28KmCBbpFgXNnmNjG9sX1hKyrvClCtI3GK3oAAt1gwBGKVXg0nS14qMgB1+0HbcQQMAGwMAYAtqNYHKTrGN/rBBWkAZA20jEp3tB30OojFStb94DW4nS94SuiFKyLbwldVeIsJHQLGEi0+kKnlXuN4SOG+/wBoxW4//9k=" + + const desiredCommands: T.ChatBotCommand[] = [ + ...(grokEnabled ? [{type: "command" as const, keyword: "grok", label: "Ask Grok"}] : []), + {type: "command", keyword: "team", label: "Switch to team"}, + ] + + // Step 1: Init main bot via bot.run() + log("Initializing main bot...") + const [chat, mainUser, mainAddress] = await bot.run({ + profile: {displayName: "Ask SimpleX Team", fullName: "", image: supportImage}, + dbOpts: config.db, + options: { + addressSettings: { + businessAddress: true, + autoAccept: true, + welcomeMessage, + }, + commands: desiredCommands, + useBotProfile: true, + updateProfile: false, + }, + events: { + acceptingBusinessRequest: (evt) => supportBot?.onBusinessRequest(evt), + newChatItems: (evt) => supportBot?.onNewChatItems(evt), + chatItemUpdated: (evt) => supportBot?.onChatItemUpdated(evt), + chatItemReaction: (evt) => supportBot?.onChatItemReaction(evt), + leftMember: (evt) => supportBot?.onLeftMember(evt), + joinedGroupMember: (evt) => supportBot?.onJoinedGroupMember(evt), + connectedToGroupMember: (evt) => supportBot?.onMemberConnected(evt), + newMemberContactReceivedInv: (evt) => supportBot?.onMemberContactReceivedInv(evt), + contactConnected: (evt) => supportBot?.onContactConnected(evt), + contactSndReady: (evt) => supportBot?.onContactSndReady(evt), + }, + }) + log(`Main bot user: ${mainUser.profile.displayName} (userId=${mainUser.userId})`) + + // Step 2: Resolve Grok profile from same ChatApi instance. + // Identify Grok strictly by the persisted userId in state.json. If no ID + // is persisted, this is a first-time run — create the user and persist. + let grokUser: T.User | null = null + if (grokEnabled) { + log("Resolving Grok profile...") + if (state.grokUserId !== undefined) { + const users = await chat.apiListUsers() + grokUser = users.find(u => u.user.userId === state.grokUserId)?.user ?? null + if (!grokUser) { + throw new Error( + `Persisted Grok userId=${state.grokUserId} not found in DB. ` + + `Either restore the user or delete state.json to re-create Grok.` + ) + } + } else { + log("Creating Grok profile...") + grokUser = await chat.apiCreateActiveUser({displayName: "Grok", fullName: "", image: grokImage}) + // apiCreateActiveUser sets Grok as active — switch back to main + await chat.apiSetActiveUser(mainUser.userId) + state.grokUserId = grokUser.userId + writeState(stateFilePath, state) + log(`Persisted Grok userId=${grokUser.userId}`) + } + + // Refresh Grok's profile if it has drifted from the canonical values. + const grokProfile: T.Profile = {displayName: "Grok", fullName: "", image: grokImage} + const currentProfile = util.fromLocalProfile(grokUser.profile) + if (currentProfile.image !== grokProfile.image || currentProfile.displayName !== grokProfile.displayName || currentProfile.fullName !== grokProfile.fullName) { + log("Grok profile changed, updating...") + await chat.apiSetActiveUser(grokUser.userId) + const summary = await chat.apiUpdateProfile(grokUser.userId, grokProfile) + await chat.apiSetActiveUser(mainUser.userId) + if (summary) { + log(`Grok profile updated: ${summary.updateSuccesses} contact(s) updated, ${summary.updateFailures} failed`) + } else { + log("Unexpected: Grok profile did not change") + } + } + log(`Grok profile: ${grokUser.profile.displayName} (userId=${grokUser.userId})`) + } + + // Step 3: Read state file + // Step 4: Enable auto-accept DM contacts + await chat.apiSetAutoAcceptMemberContacts(mainUser.userId, true) + log("Auto-accept member contacts enabled") + + // Step 5: Resolve Grok contact by ID. Avoid apiListContacts — it loads + // every contact in one response and OOMs the native binding on large DBs. + // Always restore grokContactId so the one-way gate can find and remove + // Grok members even when Grok API is disabled. + if (typeof state.grokContactId === "number") { + const found = await getContact(chat, state.grokContactId) + if (found) { + config.grokContactId = found.contactId + log(`Grok contact from state: ID=${config.grokContactId}`) + } else { + log(`Persisted Grok contact ID=${state.grokContactId} not found`) + } + } + + if (grokEnabled) { + if (config.grokContactId === null) { + log("Establishing bot↔Grok contact...") + const invLink = await chat.apiCreateLink(mainUser.userId) + // Switch to Grok profile to connect + await profileMutex.runExclusive(async () => { + await chat.apiSetActiveUser(grokUser!.userId) + await chat.apiConnectActiveUser(invLink) + await chat.apiSetActiveUser(mainUser.userId) + }) + log("Grok connecting...") + + const grokProfileName = grokUser!.profile.displayName + const evt = await chat.wait( + "contactConnected", + (e) => + e.user.userId === mainUser.userId + && e.contact.profile.displayName === grokProfileName, + 60_000, + ) + if (!evt) { + console.error(`Timeout waiting for Grok contact (60s, displayName="${grokProfileName}"). Exiting.`) + process.exit(1) + } + config.grokContactId = evt.contact.contactId + state.grokContactId = config.grokContactId + writeState(stateFilePath, state) + log(`Grok contact established: ID=${config.grokContactId}`) + } + } + + // Step 6: Resolve team group by ID. Avoid apiListGroups — it loads every + // group in one response and OOMs the native binding on large DBs. + log("Resolving team group...") + let existingGroup: T.GroupInfo | null = null + + if (typeof state.teamGroupId === "number") { + existingGroup = await getGroupInfo(chat, state.teamGroupId) + if (existingGroup) { + config.teamGroup.id = existingGroup.groupId + log(`Team group from state: ${config.teamGroup.id}:${existingGroup.groupProfile.displayName}`) + } else { + log(`Persisted team group ID=${state.teamGroupId} not found, will create`) + } + } + + const teamGroupPreferences: T.GroupPreferences = { + directMessages: {enable: T.GroupFeatureEnabled.On}, + fullDelete: {enable: T.GroupFeatureEnabled.On}, + commands: [ + {type: "command", keyword: "join", label: "Join customer chat", params: "groupId"}, + ], + } + + if (config.teamGroup.id === 0) { + log(`Creating team group "${config.teamGroup.name}"...`) + const newGroup = await chat.apiNewGroup(mainUser.userId, { + displayName: config.teamGroup.name, + fullName: "", + groupPreferences: teamGroupPreferences, + }) + config.teamGroup.id = newGroup.groupId + state.teamGroupId = config.teamGroup.id + writeState(stateFilePath, state) + log(`Team group created: ${config.teamGroup.id}:${config.teamGroup.name}`) + } else if (existingGroup) { + // Only update profile if preferences or name changed + const prefs = existingGroup.fullGroupPreferences + const needsUpdate = + existingGroup.groupProfile.displayName !== config.teamGroup.name || + prefs.directMessages?.enable !== T.GroupFeatureEnabled.On || + prefs.fullDelete?.enable !== T.GroupFeatureEnabled.On || + JSON.stringify(prefs.commands) !== JSON.stringify(teamGroupPreferences.commands) + if (needsUpdate) { + await chat.apiUpdateGroupProfile(config.teamGroup.id, { + displayName: config.teamGroup.name, + fullName: "", + groupPreferences: teamGroupPreferences, + }) + log("Team group profile updated") + } + } + + // Step 7: Ensure direct messages enabled (done via groupPreferences above) + + // Step 8: Create team group invite link (best-effort — bot works without it) + let inviteLinkCreated = false + try { + try { await chat.apiDeleteGroupLink(config.teamGroup.id) } catch {} + const teamGroupInviteLink = await chat.apiCreateGroupLink( + config.teamGroup.id, T.GroupMemberRole.Member + ) + inviteLinkCreated = true + log("Team group invite link created") + console.log(`\nTeam group invite link (expires in 10 min):\n${teamGroupInviteLink}\n`) + } catch (err) { + logError("Failed to create team group invite link (SMP relay may be unreachable). Bot will continue without it.", err) + } + + let inviteLinkDeleted = false + async function deleteInviteLink(): Promise { + if (inviteLinkDeleted) return + inviteLinkDeleted = true + try { + await profileMutex.runExclusive(async () => { + await chat.apiSetActiveUser(mainUser.userId) + await chat.apiDeleteGroupLink(config.teamGroup.id) + }) + log("Team group invite link deleted") + } catch (err) { + logError("Failed to delete invite link", err) + } + } + let inviteLinkTimer: ReturnType | undefined + if (inviteLinkCreated) { + inviteLinkTimer = setTimeout(async () => { + log("10 minutes elapsed, deleting invite link...") + await deleteInviteLink() + }, 10 * 60 * 1000) + inviteLinkTimer.unref() + } + + // Step 9: Validate team members (lookup by ID, one round-trip per member) + if (config.teamMembers.length > 0) { + log("Validating team members...") + for (const member of config.teamMembers) { + const contact = await getContact(chat, member.id) + if (!contact) { + console.error(`Team member not found: ID=${member.id}`) + process.exit(1) + } + if (contact.profile.displayName !== member.name) { + console.error(`Team member name mismatch: expected "${member.name}", got "${contact.profile.displayName}" (ID=${member.id})`) + process.exit(1) + } + log(`Team member validated: ${member.id}:${member.name}`) + } + } + + // Load Grok context and build API client only if enabled + let grokApi: GrokApiClient | null = null + if (grokEnabled) { + let contextFile = "" + if (config.contextFile) { + try { + contextFile = readFileSync(config.contextFile, "utf-8") + log(`Loaded Grok context: ${contextFile.length} chars from ${config.contextFile}`) + } catch { + log(`Warning: context file not found: ${config.contextFile}`) + } + } + grokApi = new GrokApiClient(config.grokApiKey!, contextFile) + } + + // Create SupportBot + supportBot = new SupportBot(chat, grokApi, config, mainUser.userId, grokUser?.userId ?? null, desiredCommands) + + if (mainAddress) { + supportBot.businessAddress = util.contactAddressStr(mainAddress.connLinkContact) + log(`Business address: ${supportBot.businessAddress}`) + } + + // Step 10: Register Grok event handlers (filtered by profile in handler) + if (grokEnabled) { + chat.on("receivedGroupInvitation", (evt) => supportBot?.onGrokGroupInvitation(evt)) + chat.on("connectedToGroupMember", (evt) => supportBot?.onGrokMemberConnected(evt)) + chat.on("newChatItems", (evt) => supportBot?.onGrokNewChatItems(evt)) + } + + // Step 10b: Refresh stale cards from before restart + await supportBot.cards.refreshAllCards() + + log("SupportBot initialized. Bot running.") + + // Step 11: Graceful shutdown + async function shutdown(signal: string): Promise { + log(`Received ${signal}, shutting down...`) + clearTimeout(inviteLinkTimer) + supportBot?.cards.destroy() + await deleteInviteLink() + process.exit(0) + } + process.on("SIGINT", () => shutdown("SIGINT")) + process.on("SIGTERM", () => shutdown("SIGTERM")) +} + +main().catch(err => { + logError("Fatal error", err) + process.exit(1) +}) diff --git a/apps/simplex-support-bot/src/messages.ts b/apps/simplex-support-bot/src/messages.ts new file mode 100644 index 0000000000..c35789d26b --- /dev/null +++ b/apps/simplex-support-bot/src/messages.ts @@ -0,0 +1,44 @@ +import {isWeekend} from "./util.js" + +export const welcomeMessage = `Hello! This is a *SimpleX team* support bot - not an AI. +*Join public groups* at https://simplex.chat/directory or [via directory bot](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok) +Please ask any questions about SimpleX Chat.` + +export function queueMessage(timezone: string, grokEnabled: boolean): string { + const hours = isWeekend(timezone) ? "48" : "24" + const base = `The team will reply to your message within ${hours} hours.` + if (!grokEnabled) return base + return `${base} + +If your question is about SimpleX, click /grok for an *instant Grok answer*. + +Send /team to switch back.` +} + +export const grokActivatedMessage = `*You are now chatting with Grok* - use any language.` + +export function teamAddedMessage(timezone: string, grokPresent: boolean): string { + const hours = isWeekend(timezone) ? "48" : "24" + const base = `We will reply within ${hours} hours.` + if (!grokPresent) return base + return `${base} +Grok will be answering your questions until then.` +} + +export const teamAlreadyInvitedMessage = "A team member was invited to this conversation and will reply when available." + +export const teamLockedMessage = "Only the team will now receive your messages." + +export function noTeamMembersMessage(grokEnabled: boolean): string { + return grokEnabled + ? "No team members are available yet. Please try again later or click /grok." + : "No team members are available yet. Please try again later." +} + +export const grokInvitingMessage = "Inviting Grok, please wait..." + +export const grokUnavailableMessage = "Grok is temporarily unavailable. Please try again later or send /team for a human team member." + +export const grokErrorMessage = "Sorry, I couldn't process that. Please try again or send /team for a human team member." + +export const grokNoHistoryMessage = "I just joined but couldn't see your earlier messages. Could you repeat your question?" diff --git a/apps/simplex-support-bot/src/util.ts b/apps/simplex-support-bot/src/util.ts new file mode 100644 index 0000000000..f9a2319610 --- /dev/null +++ b/apps/simplex-support-bot/src/util.ts @@ -0,0 +1,51 @@ +import {Mutex} from "async-mutex" +import {api, core} from "simplex-chat" +import {T} from "@simplex-chat/types" + +export const profileMutex = new Mutex() + +export function isChatNotFound(err: unknown, kind: "group" | "contact"): boolean { + if (!(err instanceof core.ChatAPIError)) return false + if (err.chatError?.type !== "errorStore") return false + const seType = err.chatError.storeError.type + return kind === "group" ? seType === "groupNotFound" : seType === "contactNotFound" +} + +export async function getGroupInfo(chat: api.ChatApi, groupId: number): Promise { + try { + const c = await chat.apiGetChat(T.ChatType.Group, groupId, 0) + return c.chatInfo.type === "group" ? c.chatInfo.groupInfo : null + } catch (err) { + if (isChatNotFound(err, "group")) return null + throw err + } +} + +export async function getContact(chat: api.ChatApi, contactId: number): Promise { + try { + const c = await chat.apiGetChat(T.ChatType.Direct, contactId, 0) + return c.chatInfo.type === "direct" ? c.chatInfo.contact : null + } catch (err) { + if (isChatNotFound(err, "contact")) return null + throw err + } +} + +export function isWeekend(timezone: string): boolean { + const day = new Intl.DateTimeFormat("en-US", {timeZone: timezone, weekday: "short"}).format(new Date()) + return day === "Sat" || day === "Sun" +} + +export function log(msg: string, ...args: unknown[]): void { + const ts = new Date().toISOString() + if (args.length > 0) { + console.log(`[${ts}] ${msg}`, ...args) + } else { + console.log(`[${ts}] ${msg}`) + } +} + +export function logError(msg: string, err: unknown): void { + const ts = new Date().toISOString() + console.error(`[${ts}] ERROR: ${msg}`, err) +} diff --git a/apps/simplex-support-bot/test/__mocks__/simplex-chat-types.js b/apps/simplex-support-bot/test/__mocks__/simplex-chat-types.js new file mode 100644 index 0000000000..29fc3d01a4 --- /dev/null +++ b/apps/simplex-support-bot/test/__mocks__/simplex-chat-types.js @@ -0,0 +1,12 @@ +// Mock for @simplex-chat/types — lightweight stubs + +const ChatType = {Direct: "direct", Group: "group", Local: "local"} +const GroupMemberRole = {Member: "member", Owner: "owner", Admin: "admin", Relay: "relay", Observer: "observer", Author: "author", Moderator: "moderator"} +const GroupMemberStatus = {Connected: "connected", Complete: "complete", Announced: "announced", Left: "left", Removed: "removed", Invited: "invited"} +const GroupFeatureEnabled = {On: "on", Off: "off"} +const CIDeleteMode = {Broadcast: "broadcast", Internal: "internal"} + +module.exports = { + T: {ChatType, GroupMemberRole, GroupMemberStatus, GroupFeatureEnabled, CIDeleteMode}, + CEvt: {}, +} diff --git a/apps/simplex-support-bot/test/__mocks__/simplex-chat.js b/apps/simplex-support-bot/test/__mocks__/simplex-chat.js new file mode 100644 index 0000000000..97a7b866ca --- /dev/null +++ b/apps/simplex-support-bot/test/__mocks__/simplex-chat.js @@ -0,0 +1,36 @@ +// Mock for simplex-chat — prevents native addon from loading + +function ciContentText(chatItem) { + const c = chatItem.content + if (c.type === "sndMsgContent" || c.type === "rcvMsgContent") return c.msgContent.text + return undefined +} + +function ciBotCommand(chatItem) { + const text = ciContentText(chatItem)?.trim() + if (text) { + const r = text.match(/\/([^\s]+)(.*)/) + if (r && r.length >= 3) return {keyword: r[1], params: r[2].trim()} + } + return undefined +} + +function contactAddressStr(link) { + return link.connShortLink || link.connFullLink +} + +// Mirrors core.ChatAPIError so isChatNotFound's instanceof check passes when +// MockChatApi throws. Tests should construct these directly. +class ChatAPIError extends Error { + constructor(message, chatError) { + super(message) + this.chatError = chatError + } +} + +module.exports = { + api: {ChatApi: {}}, + bot: {}, + core: {ChatAPIError}, + util: {ciContentText, ciBotCommand, contactAddressStr}, +} diff --git a/apps/simplex-support-bot/tsconfig.json b/apps/simplex-support-bot/tsconfig.json new file mode 100644 index 0000000000..821fa663e3 --- /dev/null +++ b/apps/simplex-support-bot/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["src"], + "compilerOptions": { + "declaration": true, + "forceConsistentCasingInFileNames": true, + "lib": ["ES2022"], + "module": "Node16", + "moduleResolution": "Node16", + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noEmitOnError": true, + "outDir": "dist", + "sourceMap": true, + "strict": true, + "strictNullChecks": true, + "target": "ES2022", + "types": ["node"] + } +} diff --git a/apps/simplex-support-bot/vitest.config.ts b/apps/simplex-support-bot/vitest.config.ts new file mode 100644 index 0000000000..c143572a87 --- /dev/null +++ b/apps/simplex-support-bot/vitest.config.ts @@ -0,0 +1,22 @@ +import {defineConfig} from "vitest/config" +import path from "path" + +export default defineConfig({ + test: { + globals: true, + testTimeout: 10000, + // Clear backend signals — .npmrc next to package.json otherwise injects + // npm_config_simplex_backend into every test's env, breaking sqlite-default + // assumptions in parseConfig tests. + env: { + SIMPLEX_BACKEND: "", + npm_config_simplex_backend: "", + }, + }, + resolve: { + alias: { + "simplex-chat": path.resolve(__dirname, "test/__mocks__/simplex-chat.js"), + "@simplex-chat/types": path.resolve(__dirname, "test/__mocks__/simplex-chat-types.js"), + }, + }, +}) diff --git a/assets/ASSETS_LICENSE.md b/assets/ASSETS_LICENSE.md new file mode 100644 index 0000000000..36ac54d04f --- /dev/null +++ b/assets/ASSETS_LICENSE.md @@ -0,0 +1,18 @@ +# Application Graphic Assets License + +Copyright (C) 2026 SimpleX Chat Ltd. All rights reserved. + +The graphic assets in this folder, subfolders and in other folders of this repository - including illustrations, images, icons, and visual designs - are proprietary and are not licensed under the AGPLv3 that covers the application source code. + +## Permitted use + +- Unmodified application distribution. You may use these assets as part of the SimpleX Chat application, provided the application is not modified in any way. +- Publications with permission. You may use screenshots containing these assets in publications with prior written permission from SimpleX Chat Ltd. + +## Not permitted + +All other use, including modification, redistribution, or incorporation into other works, is not permitted without prior written permission from SimpleX Chat Ltd. + +## Contact + +To request permission, contact chat@simplex.chat. diff --git a/assets/multiplatform/resources/MR/images/banner_create_link@2x.png b/assets/multiplatform/resources/MR/images/banner_create_link@2x.png new file mode 100644 index 0000000000..6f768932ad Binary files /dev/null and b/assets/multiplatform/resources/MR/images/banner_create_link@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/banner_create_link@3x.png b/assets/multiplatform/resources/MR/images/banner_create_link@3x.png new file mode 100644 index 0000000000..8fe3b9c035 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/banner_create_link@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/banner_create_link_light@2x.png b/assets/multiplatform/resources/MR/images/banner_create_link_light@2x.png new file mode 100644 index 0000000000..c7bfca381a Binary files /dev/null and b/assets/multiplatform/resources/MR/images/banner_create_link_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/banner_create_link_light@3x.png b/assets/multiplatform/resources/MR/images/banner_create_link_light@3x.png new file mode 100644 index 0000000000..a25f96d596 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/banner_create_link_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/banner_paste_link@2x.png b/assets/multiplatform/resources/MR/images/banner_paste_link@2x.png new file mode 100644 index 0000000000..44feaf8845 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/banner_paste_link@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/banner_paste_link@3x.png b/assets/multiplatform/resources/MR/images/banner_paste_link@3x.png new file mode 100644 index 0000000000..4d1a6e2fda Binary files /dev/null and b/assets/multiplatform/resources/MR/images/banner_paste_link@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/banner_paste_link_light@2x.png b/assets/multiplatform/resources/MR/images/banner_paste_link_light@2x.png new file mode 100644 index 0000000000..c34e988886 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/banner_paste_link_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/banner_paste_link_light@3x.png b/assets/multiplatform/resources/MR/images/banner_paste_link_light@3x.png new file mode 100644 index 0000000000..9d813e2c8e Binary files /dev/null and b/assets/multiplatform/resources/MR/images/banner_paste_link_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_connect_via_link_alpha@2x.png b/assets/multiplatform/resources/MR/images/card_connect_via_link_alpha@2x.png new file mode 100644 index 0000000000..3f53f89dd6 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_connect_via_link_alpha@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_connect_via_link_alpha@3x.png b/assets/multiplatform/resources/MR/images/card_connect_via_link_alpha@3x.png new file mode 100644 index 0000000000..490afadc98 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_connect_via_link_alpha@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_connect_via_link_alpha_light@2x.png b/assets/multiplatform/resources/MR/images/card_connect_via_link_alpha_light@2x.png new file mode 100644 index 0000000000..77a072dc62 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_connect_via_link_alpha_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_connect_via_link_alpha_light@3x.png b/assets/multiplatform/resources/MR/images/card_connect_via_link_alpha_light@3x.png new file mode 100644 index 0000000000..556c4d36a4 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_connect_via_link_alpha_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_create_your_public_address_alpha@2x.png b/assets/multiplatform/resources/MR/images/card_create_your_public_address_alpha@2x.png new file mode 100644 index 0000000000..b5c813009b Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_create_your_public_address_alpha@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_create_your_public_address_alpha@3x.png b/assets/multiplatform/resources/MR/images/card_create_your_public_address_alpha@3x.png new file mode 100644 index 0000000000..165e84c64a Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_create_your_public_address_alpha@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_create_your_public_address_alpha_light@2x.png b/assets/multiplatform/resources/MR/images/card_create_your_public_address_alpha_light@2x.png new file mode 100644 index 0000000000..6f133967da Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_create_your_public_address_alpha_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_create_your_public_address_alpha_light@3x.png b/assets/multiplatform/resources/MR/images/card_create_your_public_address_alpha_light@3x.png new file mode 100644 index 0000000000..38970844b7 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_create_your_public_address_alpha_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_invite_someone_privately_alpha@2x.png b/assets/multiplatform/resources/MR/images/card_invite_someone_privately_alpha@2x.png new file mode 100644 index 0000000000..3d54b2c507 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_invite_someone_privately_alpha@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_invite_someone_privately_alpha@3x.png b/assets/multiplatform/resources/MR/images/card_invite_someone_privately_alpha@3x.png new file mode 100644 index 0000000000..c0e92a91a2 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_invite_someone_privately_alpha@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_invite_someone_privately_alpha_light@2x.png b/assets/multiplatform/resources/MR/images/card_invite_someone_privately_alpha_light@2x.png new file mode 100644 index 0000000000..329fc8d6c4 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_invite_someone_privately_alpha_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_invite_someone_privately_alpha_light@3x.png b/assets/multiplatform/resources/MR/images/card_invite_someone_privately_alpha_light@3x.png new file mode 100644 index 0000000000..99fb7a45d6 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_invite_someone_privately_alpha_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_let_someone_connect_to_you_alpha@2x.png b/assets/multiplatform/resources/MR/images/card_let_someone_connect_to_you_alpha@2x.png new file mode 100644 index 0000000000..cc0446d16f Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_let_someone_connect_to_you_alpha@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_let_someone_connect_to_you_alpha@3x.png b/assets/multiplatform/resources/MR/images/card_let_someone_connect_to_you_alpha@3x.png new file mode 100644 index 0000000000..8ea447c884 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_let_someone_connect_to_you_alpha@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_let_someone_connect_to_you_alpha_light@2x.png b/assets/multiplatform/resources/MR/images/card_let_someone_connect_to_you_alpha_light@2x.png new file mode 100644 index 0000000000..b37a483be1 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_let_someone_connect_to_you_alpha_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/card_let_someone_connect_to_you_alpha_light@3x.png b/assets/multiplatform/resources/MR/images/card_let_someone_connect_to_you_alpha_light@3x.png new file mode 100644 index 0000000000..414870fc3a Binary files /dev/null and b/assets/multiplatform/resources/MR/images/card_let_someone_connect_to_you_alpha_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/connect_via_link@2x.png b/assets/multiplatform/resources/MR/images/connect_via_link@2x.png new file mode 100644 index 0000000000..24be83e066 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/connect_via_link@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/connect_via_link@3x.png b/assets/multiplatform/resources/MR/images/connect_via_link@3x.png new file mode 100644 index 0000000000..73f118580c Binary files /dev/null and b/assets/multiplatform/resources/MR/images/connect_via_link@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/connect_via_link_light@2x.png b/assets/multiplatform/resources/MR/images/connect_via_link_light@2x.png new file mode 100644 index 0000000000..8a2d8e605a Binary files /dev/null and b/assets/multiplatform/resources/MR/images/connect_via_link_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/connect_via_link_light@3x.png b/assets/multiplatform/resources/MR/images/connect_via_link_light@3x.png new file mode 100644 index 0000000000..b6ee8a4bb6 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/connect_via_link_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/connect_via_link_small@2x.png b/assets/multiplatform/resources/MR/images/connect_via_link_small@2x.png new file mode 100644 index 0000000000..b105e3be3e Binary files /dev/null and b/assets/multiplatform/resources/MR/images/connect_via_link_small@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/connect_via_link_small@3x.png b/assets/multiplatform/resources/MR/images/connect_via_link_small@3x.png new file mode 100644 index 0000000000..1e410de4b5 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/connect_via_link_small@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/connect_via_link_small_light@2x.png b/assets/multiplatform/resources/MR/images/connect_via_link_small_light@2x.png new file mode 100644 index 0000000000..73520c2f68 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/connect_via_link_small_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/connect_via_link_small_light@3x.png b/assets/multiplatform/resources/MR/images/connect_via_link_small_light@3x.png new file mode 100644 index 0000000000..565d44690b Binary files /dev/null and b/assets/multiplatform/resources/MR/images/connect_via_link_small_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/create_channel@2x.png b/assets/multiplatform/resources/MR/images/create_channel@2x.png new file mode 100644 index 0000000000..a14e4c5e11 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/create_channel@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/create_channel@3x.png b/assets/multiplatform/resources/MR/images/create_channel@3x.png new file mode 100644 index 0000000000..dfd6945d9c Binary files /dev/null and b/assets/multiplatform/resources/MR/images/create_channel@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/create_channel_light@2x.png b/assets/multiplatform/resources/MR/images/create_channel_light@2x.png new file mode 100644 index 0000000000..5dafcad62a Binary files /dev/null and b/assets/multiplatform/resources/MR/images/create_channel_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/create_channel_light@3x.png b/assets/multiplatform/resources/MR/images/create_channel_light@3x.png new file mode 100644 index 0000000000..7600906d71 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/create_channel_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/create_group@2x.png b/assets/multiplatform/resources/MR/images/create_group@2x.png new file mode 100644 index 0000000000..7fa788d8c8 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/create_group@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/create_group@3x.png b/assets/multiplatform/resources/MR/images/create_group@3x.png new file mode 100644 index 0000000000..cd4bfa45a4 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/create_group@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/create_group_light@2x.png b/assets/multiplatform/resources/MR/images/create_group_light@2x.png new file mode 100644 index 0000000000..de32b94652 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/create_group_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/create_group_light@3x.png b/assets/multiplatform/resources/MR/images/create_group_light@3x.png new file mode 100644 index 0000000000..a05610cbb9 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/create_group_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/create_profile@2x.png b/assets/multiplatform/resources/MR/images/create_profile@2x.png new file mode 100644 index 0000000000..0639186b0b Binary files /dev/null and b/assets/multiplatform/resources/MR/images/create_profile@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/create_profile@3x.png b/assets/multiplatform/resources/MR/images/create_profile@3x.png new file mode 100644 index 0000000000..1813fea83b Binary files /dev/null and b/assets/multiplatform/resources/MR/images/create_profile@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/create_profile_light@2x.png b/assets/multiplatform/resources/MR/images/create_profile_light@2x.png new file mode 100644 index 0000000000..2a3f4931e9 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/create_profile_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/create_profile_light@3x.png b/assets/multiplatform/resources/MR/images/create_profile_light@3x.png new file mode 100644 index 0000000000..9a9fd22cfb Binary files /dev/null and b/assets/multiplatform/resources/MR/images/create_profile_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/intro@2x.png b/assets/multiplatform/resources/MR/images/intro@2x.png new file mode 100644 index 0000000000..970d68927c Binary files /dev/null and b/assets/multiplatform/resources/MR/images/intro@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/intro@3x.png b/assets/multiplatform/resources/MR/images/intro@3x.png new file mode 100644 index 0000000000..cbd56771c7 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/intro@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/intro_light@2x.png b/assets/multiplatform/resources/MR/images/intro_light@2x.png new file mode 100644 index 0000000000..938a0b1755 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/intro_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/intro_light@3x.png b/assets/multiplatform/resources/MR/images/intro_light@3x.png new file mode 100644 index 0000000000..569c56fd29 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/intro_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/network_commitments@2x.png b/assets/multiplatform/resources/MR/images/network_commitments@2x.png new file mode 100644 index 0000000000..4b58a588d3 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/network_commitments@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/network_commitments@3x.png b/assets/multiplatform/resources/MR/images/network_commitments@3x.png new file mode 100644 index 0000000000..9b80a623a1 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/network_commitments@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/network_commitments_light@2x.png b/assets/multiplatform/resources/MR/images/network_commitments_light@2x.png new file mode 100644 index 0000000000..5e07e0afdb Binary files /dev/null and b/assets/multiplatform/resources/MR/images/network_commitments_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/network_commitments_light@3x.png b/assets/multiplatform/resources/MR/images/network_commitments_light@3x.png new file mode 100644 index 0000000000..aadfa0288d Binary files /dev/null and b/assets/multiplatform/resources/MR/images/network_commitments_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/one_time_link@2x.png b/assets/multiplatform/resources/MR/images/one_time_link@2x.png new file mode 100644 index 0000000000..8b3ba2f0ee Binary files /dev/null and b/assets/multiplatform/resources/MR/images/one_time_link@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/one_time_link@3x.png b/assets/multiplatform/resources/MR/images/one_time_link@3x.png new file mode 100644 index 0000000000..de87789d1b Binary files /dev/null and b/assets/multiplatform/resources/MR/images/one_time_link@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/one_time_link_light@2x.png b/assets/multiplatform/resources/MR/images/one_time_link_light@2x.png new file mode 100644 index 0000000000..3b0c02209b Binary files /dev/null and b/assets/multiplatform/resources/MR/images/one_time_link_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/one_time_link_light@3x.png b/assets/multiplatform/resources/MR/images/one_time_link_light@3x.png new file mode 100644 index 0000000000..87360c3135 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/one_time_link_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/one_time_link_small@2x.png b/assets/multiplatform/resources/MR/images/one_time_link_small@2x.png new file mode 100644 index 0000000000..f9d94cf265 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/one_time_link_small@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/one_time_link_small@3x.png b/assets/multiplatform/resources/MR/images/one_time_link_small@3x.png new file mode 100644 index 0000000000..2dac7ef638 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/one_time_link_small@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/one_time_link_small_light@2x.png b/assets/multiplatform/resources/MR/images/one_time_link_small_light@2x.png new file mode 100644 index 0000000000..916bdaa007 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/one_time_link_small_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/one_time_link_small_light@3x.png b/assets/multiplatform/resources/MR/images/one_time_link_small_light@3x.png new file mode 100644 index 0000000000..1ed8194bc9 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/one_time_link_small_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/simplex_address@2x.png b/assets/multiplatform/resources/MR/images/simplex_address@2x.png new file mode 100644 index 0000000000..237c125c62 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/simplex_address@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/simplex_address@3x.png b/assets/multiplatform/resources/MR/images/simplex_address@3x.png new file mode 100644 index 0000000000..8f5606cbbc Binary files /dev/null and b/assets/multiplatform/resources/MR/images/simplex_address@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/simplex_address_light@2x.png b/assets/multiplatform/resources/MR/images/simplex_address_light@2x.png new file mode 100644 index 0000000000..a58ebae39c Binary files /dev/null and b/assets/multiplatform/resources/MR/images/simplex_address_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/simplex_address_light@3x.png b/assets/multiplatform/resources/MR/images/simplex_address_light@3x.png new file mode 100644 index 0000000000..aae91169ef Binary files /dev/null and b/assets/multiplatform/resources/MR/images/simplex_address_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/simplex_address_small@2x.png b/assets/multiplatform/resources/MR/images/simplex_address_small@2x.png new file mode 100644 index 0000000000..6dddbbd377 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/simplex_address_small@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/simplex_address_small@3x.png b/assets/multiplatform/resources/MR/images/simplex_address_small@3x.png new file mode 100644 index 0000000000..45471e9c50 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/simplex_address_small@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/simplex_address_small_light@2x.png b/assets/multiplatform/resources/MR/images/simplex_address_small_light@2x.png new file mode 100644 index 0000000000..a1cdfc4652 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/simplex_address_small_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/simplex_address_small_light@3x.png b/assets/multiplatform/resources/MR/images/simplex_address_small_light@3x.png new file mode 100644 index 0000000000..f54baf5dc4 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/simplex_address_small_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/your_network@2x.png b/assets/multiplatform/resources/MR/images/your_network@2x.png new file mode 100644 index 0000000000..b7b5d6aa87 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/your_network@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/your_network@3x.png b/assets/multiplatform/resources/MR/images/your_network@3x.png new file mode 100644 index 0000000000..9ff0e77a86 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/your_network@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/your_network_light@2x.png b/assets/multiplatform/resources/MR/images/your_network_light@2x.png new file mode 100644 index 0000000000..12031202d8 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/your_network_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/your_network_light@3x.png b/assets/multiplatform/resources/MR/images/your_network_light@3x.png new file mode 100644 index 0000000000..56b7f20c59 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/your_network_light@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/your_profile@2x.png b/assets/multiplatform/resources/MR/images/your_profile@2x.png new file mode 100644 index 0000000000..81e8e1a6b0 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/your_profile@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/your_profile@3x.png b/assets/multiplatform/resources/MR/images/your_profile@3x.png new file mode 100644 index 0000000000..01ea5da43c Binary files /dev/null and b/assets/multiplatform/resources/MR/images/your_profile@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/your_profile_light@2x.png b/assets/multiplatform/resources/MR/images/your_profile_light@2x.png new file mode 100644 index 0000000000..91671dadb0 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/your_profile_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/your_profile_light@3x.png b/assets/multiplatform/resources/MR/images/your_profile_light@3x.png new file mode 100644 index 0000000000..8e1d3fd15e Binary files /dev/null and b/assets/multiplatform/resources/MR/images/your_profile_light@3x.png differ diff --git a/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.md b/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.md new file mode 100644 index 0000000000..4a63cb87ca --- /dev/null +++ b/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.md @@ -0,0 +1,71 @@ +--- +layout: layouts/article.html +title: "SimpleX Channels, SimpleX Network Consortium and Community Crowdfunding - to Preserve Freedom of Speech" +date: 2026-04-30 +previewBody: blog_previews/20260430.html +image: images/20260430-home.png +imageLight: images/20260430-home-light.png +permalink: "/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html" +--- + +# SimpleX Channels, SimpleX Network Consortium and Community Crowdfunding — to Preserve Freedom of Speech + +**Published:** Apr 30, 2026 + +Freedom of speech needs infrastructure that protects it by design — not only the protocols and servers, but the governance and funding to support them. + +## SimpleX Channels — more public, more freedom, more private + + + +v6.5 release[^release] brings SimpleX Channels: a new model for online publishing built for participation privacy. + +Channel content is visible to chat relay operators. And each channel uses multiple relays, so no single relay can block the channel[^preset]. + +But the real identities of channel owners and subscribers are unknown to relay operators, to each other, and to the network. This is important for freedom of speech and for our ability to say the truth[^wilde]. + +This is the opposite of the usual approach: instead of trying (and failing [^public]) to hide publicly available content from operators while exposing participants, we designed the protocols to protect people. Anybody can join a public channel via its link and see what is sent, but not who sent it, and not who else is reading. This is win-win for both users and chat relays operators. Users' privacy is protected, operators can decide what content to deliver in public spaces, and anybody can run chat relays. + +This is only possible because SimpleX network was built without user profile identifiers of any kind. You can't add participation privacy to a network that identifies its users — as you can't add privacy to a messenger built on phone numbers. + +v6.5 is the first beta version of channels: +- channel owners hold their own channel keys, +- each channel uses multiple relays for reliability, +- publishers can run their own chat relays, +- channels can be added to our [SimpleX Directory](https://simplex.chat/directory/). + +This release is a beginning of a very important new layer of SimpleX Network. Read more about channels in [whitepaper](https://github.com/simplex-chat/simplex-chat/blob/master/docs/protocol/channels-overview.md): their purpose, architecture, security model and planned future work. + +## SimpleX Network Consortium — to preserve network independence + +No single company should control protocols and network that people depend on to speak freely. If a network is run by a single company, the network has a risk that business and users interests diverge — if it happens, users lose. + +To protect network neutrality and make sure its protocols and intellectual property are available to the users, we're launching [SimpleX Network Consortium](https://simplexnetwork.org) within a few months — the agreement between the new SimpleX Network Foundation and SimpleX Chat company that will govern protocols and licensing — perpetual, irrevocable, surviving if any party is sold or shut down. Other organizations will join. + +We are currently forming the board for SimpleX Network Foundation — initially, [Heather Meeker](https://heathermeeker.com/about-me/), who drafted the Consortium agreement, and several other people will join. We will announce the board soon. + +As the power over the network protocols moves away from the company, it cannot move back[^ulysses]. It is a structural guarantee — the same principle we applied to privacy. + +## Community Crowdfunding + +We've seen open-source privacy-focussed projects die without funding, or worse — being captured by their sponsors. We've seen "don't be evil" companies get lured off course by growth and board pressure. Neither pure ideology nor pure commerce survives the long run alone. + +So we're building both: a governance structure and a real business. The governance protects the network neutrality. The commercial model funds the network and makes our and other businesses on the network profitable, ensuring their independence. Neither works without the other. + +We recently published [a preliminary design of commercial model](https://simplex.chat/credits/) — private Community Credits that fund servers, development, and governance without surveillance or speculation. The full investment case will be published when crowdfunding launches. + +You can *register your interest* to participate in crowdfunding here: https://simplexchat.typeform.com/crowdfunding + +Join the channel for updates [here](https://smp10.simplex.im/c#q09nMBmWFGz1m2TvgfZFaEOG5D2a7Ma9mSkl6pHXEsg) — you must install v6.5 to join it — or you can join a [read-only group](https://smp12.simplex.im/g#gJzy7ETpuvltqARIB73TQUpJ11Lz4Xpl9xeH9qNoGCg) from the previous app versions. + +_Disclaimer: SimpleX Chat is testing the waters for a possible Reg CF offering. We’re not asking for or accepting any money right now, and we won’t accept any if sent. We can’t accept any offers to buy securities or take any payments until the official filing is done and it’s live through a regulated platform. Our testing the waters and your possible indications of interest doesn’t create any obligation or commitment of any kind._ + +[^release]: v6.5 release also improved how new users make the first connection, increased security of sending web links, and has many other improvements — see *What's new* in the app or full release notes. + +[^preset]: Currently there is only one preset operator of chat relays in the app. It will change in the next release. + +[^wilde]: Oscar Wilde wrote: *"Man is least himself when he talks in his own person. Give him a mask, and he will tell you the truth"*. Privacy is essential for our ability to say the truth, and without truth we cannot survive as society. + +[^public]: From whitepaper: any channel joinable via a public link, whether encrypted or not, must be considered completely public — the cost of joining through automated means has collapsed with large language models. End-to-end encrypting such content provides no privacy; it only undermines users' security by creating false expectations and increases infrastructure operators' risks by making them unable to see what they deliver. + +[^ulysses]: Ulysses pact — adding constraints to reduce future options. Sé Reed used this analogy for the WordPress Foundation: tying the project to the mast before the siren songs of commercial capture (https://www.wpwatercooler.com/wpwatercooler/ep484-whose-wordpress-is-it-anyway/). diff --git a/blog/images/20260430-channel.png b/blog/images/20260430-channel.png new file mode 100644 index 0000000000..2600958dd0 Binary files /dev/null and b/blog/images/20260430-channel.png differ diff --git a/blog/images/20260430-home-light.png b/blog/images/20260430-home-light.png new file mode 100644 index 0000000000..c06f12e5e6 Binary files /dev/null and b/blog/images/20260430-home-light.png differ diff --git a/blog/images/20260430-home.png b/blog/images/20260430-home.png new file mode 100644 index 0000000000..df82b24c9e Binary files /dev/null and b/blog/images/20260430-home.png differ diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index b5d49077c0..ed50cdbb9a 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -32,6 +32,7 @@ This file is generated automatically. - [APINewGroup](#apinewgroup) - [APINewPublicGroup](#apinewpublicgroup) - [APIGetGroupRelays](#apigetgrouprelays) +- [APIAddGroupRelays](#apiaddgrouprelays) - [APIUpdateGroupProfile](#apiupdategroupprofile) [Group link commands](#group-link-commands) @@ -51,6 +52,7 @@ This file is generated automatically. [Chat commands](#chat-commands) - [APIListContacts](#apilistcontacts) - [APIListGroups](#apilistgroups) +- [APIGetChats](#apigetchats) - [APIDeleteChat](#apideletechat) - [APISetGroupCustomData](#apisetgroupcustomdata) - [APISetContactCustomData](#apisetcontactcustomdata) @@ -983,6 +985,11 @@ PublicGroupCreated: Public group created. - groupLink: [GroupLink](./TYPES.md#grouplink) - groupRelays: [[GroupRelay](./TYPES.md#grouprelay)] +PublicGroupCreationFailed: Public group creation failed. +- type: "publicGroupCreationFailed" +- user: [User](./TYPES.md#user) +- addRelayResults: [[AddRelayResult](./TYPES.md#addrelayresult)] + ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1028,6 +1035,51 @@ ChatCmdError: Command error (only used in WebSockets API). --- +### APIAddGroupRelays + +Add relays to group. + +*Network usage*: interactive. + +**Parameters**: +- groupId: int64 +- relayIds: [int64] + +**Syntax**: + +``` +/_add relays # [,...] +``` + +```javascript +'/_add relays #' + groupId + ' ' + relayIds.join(',') // JavaScript +``` + +```python +'/_add relays #' + str(groupId) + ' ' + ','.join(map(str, relayIds)) # Python +``` + +**Responses**: + +GroupRelaysAdded: Group relays added. +- type: "groupRelaysAdded" +- user: [User](./TYPES.md#user) +- groupInfo: [GroupInfo](./TYPES.md#groupinfo) +- groupLink: [GroupLink](./TYPES.md#grouplink) +- groupRelays: [[GroupRelay](./TYPES.md#grouprelay)] + +GroupRelaysAddFailed: Group relays add failed. +- type: "groupRelaysAddFailed" +- user: [User](./TYPES.md#user) +- addRelayResults: [[AddRelayResult](./TYPES.md#addrelayresult)] + +ChatCmdError: Command error (only used in WebSockets API). +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + +--- + + ### APIUpdateGroupProfile Update group profile. @@ -1280,6 +1332,8 @@ Determine SimpleX link type and if the bot is already connected via this link. **Parameters**: - userId: int64 - connectionLink: string? +- resolveKnown: bool +- linkOwnerSig: [LinkOwnerSig](./TYPES.md#linkownersig)? **Syntax**: @@ -1567,6 +1621,46 @@ ChatCmdError: Command error (only used in WebSockets API). --- +### APIGetChats + +Get chat previews. Supports time-based pagination — use this instead of APIListContacts / APIListGroups when scanning at scale (those load every record into memory and fail on large databases). + +*Network usage*: no. + +**Parameters**: +- userId: int64 +- pendingConnections: bool +- pagination: [PaginationByTime](./TYPES.md#paginationbytime) +- query: [ChatListQuery](./TYPES.md#chatlistquery) + +**Syntax**: + +``` +/_get chats [ pcc=on] +``` + +```javascript +'/_get chats ' + userId + (pendingConnections ? ' pcc=on' : '') + ' ' + PaginationByTime.cmdString(pagination) + ' ' + JSON.stringify(query) // JavaScript +``` + +```python +'/_get chats ' + str(userId) + (' pcc=on' if pendingConnections else '') + ' ' + str(pagination) + ' ' + json.dumps(query) # Python +``` + +**Responses**: + +ApiChats: Chat previews (paginated). Use this instead of CRContactsList / CRGroupsList when scanning at scale.. +- type: "apiChats" +- user: [User](./TYPES.md#user) +- chats: [[AChat](./TYPES.md#achat)] + +ChatCmdError: Command error (only used in WebSockets API). +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + +--- + + ### APIDeleteChat Delete chat. diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index c5734c2eda..04d8acb84d 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -5,6 +5,7 @@ This file is generated automatically. - [ACIReaction](#acireaction) - [AChat](#achat) - [AChatItem](#achatitem) +- [AddRelayResult](#addrelayresult) - [AddressSettings](#addresssettings) - [AgentCryptoError](#agentcryptoerror) - [AgentErrorType](#agenterrortype) @@ -40,6 +41,7 @@ This file is generated automatically. - [ChatInfo](#chatinfo) - [ChatItem](#chatitem) - [ChatItemDeletion](#chatitemdeletion) +- [ChatListQuery](#chatlistquery) - [ChatPeerType](#chatpeertype) - [ChatRef](#chatref) - [ChatSettings](#chatsettings) @@ -51,6 +53,7 @@ This file is generated automatically. - [Color](#color) - [CommandError](#commanderror) - [CommandErrorType](#commanderrortype) +- [CommentsGroupPreference](#commentsgrouppreference) - [ComposedMessage](#composedmessage) - [ConnStatus](#connstatus) - [ConnType](#conntype) @@ -69,6 +72,7 @@ This file is generated automatically. - [CreatedConnLink](#createdconnlink) - [CryptoFile](#cryptofile) - [CryptoFileArgs](#cryptofileargs) +- [DroppedMsg](#droppedmsg) - [E2EInfo](#e2einfo) - [ErrorType](#errortype) - [FeatureAllowed](#featureallowed) @@ -92,6 +96,7 @@ This file is generated automatically. - [GroupInfo](#groupinfo) - [GroupKeys](#groupkeys) - [GroupLink](#grouplink) +- [GroupLinkOwner](#grouplinkowner) - [GroupLinkPlan](#grouplinkplan) - [GroupMember](#groupmember) - [GroupMemberAdmission](#groupmemberadmission) @@ -115,6 +120,7 @@ This file is generated automatically. - [InvitationLinkPlan](#invitationlinkplan) - [InvitedBy](#invitedby) - [LinkContent](#linkcontent) +- [LinkOwnerSig](#linkownersig) - [LinkPreview](#linkpreview) - [LocalProfile](#localprofile) - [MemberCriteria](#membercriteria) @@ -130,6 +136,8 @@ This file is generated automatically. - [NetworkError](#networkerror) - [NewUser](#newuser) - [NoteFolder](#notefolder) +- [OwnerVerification](#ownerverification) +- [PaginationByTime](#paginationbytime) - [PendingContactConnection](#pendingcontactconnection) - [PrefEnabled](#prefenabled) - [Preferences](#preferences) @@ -148,6 +156,7 @@ This file is generated automatically. - [RcvFileStatus](#rcvfilestatus) - [RcvFileTransfer](#rcvfiletransfer) - [RcvGroupEvent](#rcvgroupevent) +- [RcvMsgError](#rcvmsgerror) - [RelayProfile](#relayprofile) - [RelayStatus](#relaystatus) - [ReportReason](#reportreason) @@ -164,6 +173,7 @@ This file is generated automatically. - [SrvError](#srverror) - [StoreError](#storeerror) - [SubscriptionStatus](#subscriptionstatus) +- [SupportGroupPreference](#supportgrouppreference) - [SwitchPhase](#switchphase) - [TimedMessagesGroupPreference](#timedmessagesgrouppreference) - [TimedMessagesPreference](#timedmessagespreference) @@ -215,6 +225,15 @@ This file is generated automatically. - chatItem: [ChatItem](#chatitem) +--- + +## AddRelayResult + +**Record type**: +- relay: [UserChatRelay](#userchatrelay) +- relayError: [ChatError](#chaterror)? + + --- ## AddressSettings @@ -453,6 +472,10 @@ RcvDecryptionError: - msgDecryptError: [MsgDecryptError](#msgdecrypterror) - msgCount: word32 +RcvMsgError: +- type: "rcvMsgError" +- rcvMsgError: [RcvMsgError](#rcvmsgerror) + RcvGroupInvitation: - type: "rcvGroupInvitation" - groupInvitation: [CIGroupInvitation](#cigroupinvitation) @@ -1303,6 +1326,22 @@ Message deletion result. - toChatItem: [AChatItem](#achatitem)? +--- + +## ChatListQuery + +**Discriminated union type**: + +Filters: +- type: "filters" +- favorite: bool +- unread: bool + +Search: +- type: "search" +- search: string + + --- ## ChatPeerType @@ -1477,6 +1516,15 @@ LARGE: - type: "LARGE" +--- + +## CommentsGroupPreference + +**Record type**: +- enable: [GroupFeatureEnabled](#groupfeatureenabled) +- duration: int? + + --- ## ComposedMessage @@ -1679,6 +1727,7 @@ Error: Ok: - type: "ok" - contactSLinkData_: [ContactShortLinkData](#contactshortlinkdata)? +- ownerVerification: [OwnerVerification](#ownerverification)? OwnLink: - type: "ownLink" @@ -1800,11 +1849,21 @@ connFullLink + ((' ' + connShortLink) if connShortLink is not None else '') # Py - fileNonce: string +--- + +## DroppedMsg + +**Record type**: +- brokerTs: UTCTime +- attempts: int + + --- ## E2EInfo **Record type**: +- public: bool? - pqEnabled: bool? @@ -2066,7 +2125,9 @@ Phone: - simplexLinks: [RoleGroupPreference](#rolegrouppreference) - reports: [GroupPreference](#grouppreference) - history: [GroupPreference](#grouppreference) +- support: [SupportGroupPreference](#supportgrouppreference) - sessions: [RoleGroupPreference](#rolegrouppreference) +- comments: [CommentsGroupPreference](#commentsgrouppreference) - commands: [[ChatBotCommand](#chatbotcommand)] @@ -2156,7 +2217,9 @@ MemberSupport: - "simplexLinks" - "reports" - "history" +- "support" - "sessions" +- "comments" --- @@ -2221,6 +2284,15 @@ MemberSupport: - acceptMemberRole: [GroupMemberRole](#groupmemberrole) +--- + +## GroupLinkOwner + +**Record type**: +- memberId: string +- memberKey: string + + --- ## GroupLinkPlan @@ -2231,6 +2303,7 @@ Ok: - type: "ok" - groupSLinkInfo_: [GroupShortLinkInfo](#groupshortlinkinfo)? - groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)? +- ownerVerification: [OwnerVerification](#ownerverification)? OwnLink: - type: "ownLink" @@ -2246,6 +2319,13 @@ ConnectingProhibit: Known: - type: "known" - groupInfo: [GroupInfo](#groupinfo) +- groupUpdated: bool +- ownerVerification: [OwnerVerification](#ownerverification)? +- linkOwners: [[GroupLinkOwner](#grouplinkowner)] + +NoRelays: +- type: "noRelays" +- groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)? --- @@ -2372,7 +2452,9 @@ Known: - simplexLinks: [RoleGroupPreference](#rolegrouppreference)? - reports: [GroupPreference](#grouppreference)? - history: [GroupPreference](#grouppreference)? +- support: [SupportGroupPreference](#supportgrouppreference)? - sessions: [RoleGroupPreference](#rolegrouppreference)? +- comments: [CommentsGroupPreference](#commentsgrouppreference)? - commands: [[ChatBotCommand](#chatbotcommand)]? @@ -2464,6 +2546,7 @@ Public: **Enum type**: - "channel" +- "group" --- @@ -2495,6 +2578,7 @@ Public: Ok: - type: "ok" - contactSLinkData_: [ContactShortLinkData](#contactshortlinkdata)? +- ownerVerification: [OwnerVerification](#ownerverification)? OwnLink: - type: "ownLink" @@ -2547,6 +2631,16 @@ Unknown: - json: JSONObject +--- + +## LinkOwnerSig + +**Record type**: +- ownerId: string? +- chatBinding: string +- ownerSig: string + + --- ## LinkPreview @@ -2652,6 +2746,7 @@ Chat: - type: "chat" - text: string - chatLink: [MsgChatLink](#msgchatlink) +- ownerSig: [LinkOwnerSig](#linkownersig)? Unknown: - type: "unknown" @@ -2800,6 +2895,45 @@ SubscribeError: - unread: bool +--- + +## OwnerVerification + +**Discriminated union type**: + +Verified: +- type: "verified" + +Failed: +- type: "failed" +- reason: string + + +--- + +## PaginationByTime + +**Discriminated union type**: + +Last: +- type: "last" +- count: int + +**Syntax**: + +``` +count= +``` + +```javascript +'count=' + count // JavaScript +``` + +```python +'count=' + str(count) # Python +``` + + --- ## PendingContactConnection @@ -3177,6 +3311,21 @@ MsgBadSignature: - type: "msgBadSignature" +--- + +## RcvMsgError + +**Discriminated union type**: + +Dropped: +- type: "dropped" +- attempts: int + +ParseError: +- type: "parseError" +- parseError: string + + --- ## RelayProfile @@ -3197,6 +3346,7 @@ MsgBadSignature: - "invited" - "accepted" - "active" +- "inactive" --- @@ -3246,6 +3396,7 @@ A_CRYPTO: A_DUPLICATE: - type: "A_DUPLICATE" +- droppedMsg_: [DroppedMsg](#droppedmsg)? A_QUEUE: - type: "A_QUEUE" @@ -3793,6 +3944,14 @@ NoSub: - type: "noSub" +--- + +## SupportGroupPreference + +**Record type**: +- enable: [GroupFeatureEnabled](#groupfeatureenabled) + + --- ## SwitchPhase diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 4c9b46c112..6c3224c56e 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -117,8 +117,9 @@ chatCommandsDocsData = ("APILeaveGroup", [], "Leave group.", ["CRLeftMemberUser", "CRChatCmdError"], [], Just UNBackground, "/_leave #" <> Param "groupId"), ("APIListMembers", [], "Get group members.", ["CRGroupMembers", "CRChatCmdError"], [], Nothing, "/_members #" <> Param "groupId"), ("APINewGroup", [], "Create group.", ["CRGroupCreated", "CRChatCmdError"], [], Nothing, "/_group " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False) <> " " <> Json "groupProfile"), - ("APINewPublicGroup", [], "Create public group.", ["CRPublicGroupCreated", "CRChatCmdError"], [], Just UNInteractive, "/_public group " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False) <> " " <> Join ',' "relayIds" <> " " <> Json "groupProfile"), + ("APINewPublicGroup", [], "Create public group.", ["CRPublicGroupCreated", "CRPublicGroupCreationFailed", "CRChatCmdError"], [], Just UNInteractive, "/_public group " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False) <> " " <> Join ',' "relayIds" <> " " <> Json "groupProfile"), ("APIGetGroupRelays", [], "Get group relays.", ["CRGroupRelays", "CRChatCmdError"], [], Nothing, "/_get relays #" <> Param "groupId"), + ("APIAddGroupRelays", [], "Add relays to group.", ["CRGroupRelaysAdded", "CRGroupRelaysAddFailed", "CRChatCmdError"], [], Just UNInteractive, "/_add relays #" <> Param "groupId" <> " " <> Join ',' "relayIds"), ("APIUpdateGroupProfile", [], "Update group profile.", ["CRGroupUpdated", "CRChatCmdError"], [], Just UNBackground, "/_group_profile #" <> Param "groupId" <> " " <> Json "groupProfile") ] ), @@ -144,6 +145,7 @@ chatCommandsDocsData = "Commands to list and delete conversations.", [ ("APIListContacts", [], "Get contacts.", ["CRContactsList", "CRChatCmdError"], [], Nothing, "/_contacts " <> Param "userId"), ("APIListGroups", [], "Get groups.", ["CRGroupsList", "CRChatCmdError"], [], Nothing, "/_groups " <> Param "userId" <> Optional "" (" @" <> Param "$0") "contactId_" <> Optional "" (" " <> Param "$0") "search"), + ("APIGetChats", [], "Get chat previews. Supports time-based pagination — use this instead of APIListContacts / APIListGroups when scanning at scale (those load every record into memory and fail on large databases).", ["CRApiChats", "CRChatCmdError"], [], Nothing, "/_get chats " <> Param "userId" <> OnOffParam "pcc" "pendingConnections" (Just False) <> " " <> Param "pagination" <> " " <> Json "query"), ("APIDeleteChat", [], "Delete chat.", ["CRContactDeleted", "CRContactConnectionDeleted", "CRGroupDeletedUser", "CRChatCmdError"], [], Just UNBackground, "/_delete " <> Param "chatRef" <> " " <> Param "chatDeleteMode"), ("APISetGroupCustomData", [], "Set group custom data.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set custom #" <> Param "groupId" <> Optional "" (" " <> Json "$0") "customData"), ("APISetContactCustomData", [], "Set contact custom data.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set custom @" <> Param "contactId" <> Optional "" (" " <> Json "$0") "customData"), @@ -283,6 +285,7 @@ cliCommands = "SetUserGroupReceipts", "SetUserAutoAcceptMemberContacts", "SetUserTimedMessages", + "SharePublicGroup", "ShowChatItem", "ShowChatItemInfo", "ShowGroupDescription", @@ -357,7 +360,6 @@ undocumentedCommands = "APIGetChatItemInfo", "APIGetChatItems", "APIGetChatItemTTL", - "APIGetChats", "APIGetChatTags", "APIGetConnNtfMessages", "APIGetContactCode", @@ -408,6 +410,7 @@ undocumentedCommands = "APISetUserGroupReceipts", "APISetUserServers", "APISetUserUIThemes", + "APIShareChatMsgContent", "APIStandaloneFileInfo", "APIStorageEncryption", "APISuspendChat", diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index 873ca5eb97..55f12f0a0a 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -69,7 +69,10 @@ chatResponsesDocsData = ("CRGroupLinkDeleted", ""), ("CRGroupCreated", ""), ("CRPublicGroupCreated", ""), + ("CRPublicGroupCreationFailed", ""), ("CRGroupRelays", ""), + ("CRGroupRelaysAdded", ""), + ("CRGroupRelaysAddFailed", ""), ("CRGroupMembers", ""), ("CRGroupUpdated", ""), ("CRGroupsList", "Groups"), @@ -94,9 +97,9 @@ chatResponsesDocsData = ("CRUserDeletedMembers", "Members deleted"), ("CRUserProfileUpdated", "User profile updated"), ("CRUserProfileNoChange", "User profile was not changed"), - ("CRUsersList", "Users") + ("CRUsersList", "Users"), + ("CRApiChats", "Chat previews (paginated). Use this instead of CRContactsList / CRGroupsList when scanning at scale.") -- ("CRApiChat", "Chat and messages"), - -- ("CRApiChats", "Chats with the most recent messages"), -- ("CRChatCleared", ""), -- ("CRChatItemInfo", "Message information"), -- ("CRChatItems", "The most recent messages"), @@ -119,7 +122,6 @@ undocumentedResponses = "CRAgentWorkersDetails", "CRAgentWorkersSummary", "CRApiChat", - "CRApiChats", "CRAppSettings", "CRArchiveExported", "CRArchiveImported", @@ -132,6 +134,7 @@ undocumentedResponses = "CRChatItemInfo", "CRChatItems", "CRChatItemTTL", + "CRChatMsgContent", "CRChatRelayTestResult", "CRChats", "CRConnectionsDiff", diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 37fc6121ce..be4a55835a 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -202,6 +202,7 @@ chatTypesDocsData = (sti @(ContactUserPref SimplePreference), STUnion, "CUP", [], "", ""), (sti @(ContactUserPreference SimplePreference), STRecord, "", [], "", ""), (sti @(CreatedConnLink 'CMContact), STRecord, "", [], Param "connFullLink" <> Optional "" (" " <> Param "$0") "connShortLink", ""), + (sti @AddRelayResult, STRecord, "", [], "", ""), (sti @AddressSettings, STRecord, "", [], "", ""), (sti @AgentCryptoError, STUnion, "", ["RATCHET_EARLIER", "RATCHET_SKIPPED"], "", ""), -- TODO add fields to types (sti @AgentErrorType, STUnion, "", [], "", ""), @@ -237,6 +238,7 @@ chatTypesDocsData = (sti @Color, STEnum, "", [], "", ""), (sti @CommandError, STUnion, "", [], "", ""), (sti @CommandErrorType, STUnion, "", [], "", ""), + (sti @CommentsGroupPreference, STRecord, "", [], "", ""), (sti @ComposedMessage, STRecord, "", [], "", ""), (sti @Connection, STRecord, "", [], "", ""), (sti @ConnectionEntity, STUnion, "", [], "", ""), @@ -252,6 +254,7 @@ chatTypesDocsData = (sti @ContactUserPreferences, STRecord, "", [], "", ""), (sti @CryptoFile, STRecord, "", [], "", ""), (sti @CryptoFileArgs, STRecord, "", [], "", ""), + (sti @DroppedMsg, STRecord, "", [], "", ""), (sti @E2EInfo, STRecord, "", [], "", ""), (sti @ErrorType, STUnion, "", [], "", ""), (sti @FeatureAllowed, STEnum, "FA", [], "", ""), @@ -275,6 +278,7 @@ chatTypesDocsData = (sti @GroupKeys, STRecord, "", [], "", ""), (sti @GroupRootKey, STUnion, "GRK", [], "", ""), (sti @GroupLink, STRecord, "", [], "", ""), + (sti @GroupLinkOwner, STRecord, "", [], "", ""), (sti @GroupLinkPlan, STUnion, "GLP", [], "", ""), (sti @GroupMember, STRecord, "", [], "", ""), (sti @GroupMemberAdmission, STRecord, "", [], "", ""), @@ -291,12 +295,13 @@ chatTypesDocsData = (sti @GroupShortLinkInfo, STRecord, "", [], "", ""), (sti @GroupSummary, STRecord, "", [], "", ""), (sti @GroupSupportChat, STRecord, "", [], "", ""), - (sti @GroupType, STEnum1, "GT", ["GTUnknown"], "", ""), + (sti @GroupType, STEnum, "GT", ["GTUnknown"], "", ""), (sti @HandshakeError, STEnum, "", [], "", ""), (sti @InlineFileMode, STEnum, "IFM", [], "", ""), (sti @InvitationLinkPlan, STUnion, "ILP", [], "", ""), (sti @InvitedBy, STUnion, "IB", [], "", ""), (sti @LinkContent, STUnion, "LC", [], "", ""), + (sti @LinkOwnerSig, STRecord, "", [], "", ""), (sti @LinkPreview, STRecord, "", [], "", ""), (sti @LocalProfile, STRecord, "", [], "", ""), (sti @MemberCriteria, STEnum1, "MC", [], "", ""), @@ -312,6 +317,7 @@ chatTypesDocsData = (sti @NetworkError, STUnion, "NE", [], "", ""), (sti @NewUser, STRecord, "", [], "", ""), (sti @NoteFolder, STRecord, "", [], "", ""), + (sti @OwnerVerification, STUnion, "OV", [], "", ""), (sti @PendingContactConnection, STRecord, "", [], "", ""), (sti @PrefEnabled, STRecord, "", [], "", ""), (sti @Preferences, STRecord, "", [], "", ""), @@ -331,6 +337,7 @@ chatTypesDocsData = (sti @RcvFileStatus, STUnion, "RFS", [], "", ""), (sti @RcvFileTransfer, STRecord, "", [], "", ""), (sti @RcvGroupEvent, STUnion, "RGE", [], "", ""), + (sti @RcvMsgError, STUnion, "RME", [], "", ""), (sti @RelayProfile, STRecord, "", [], "", ""), (sti @RelayStatus, STEnum, "RS", [], "", ""), (sti @ReportReason, STEnum' (dropPfxSfx "RR" ""), "", ["RRUnknown"], "", ""), @@ -347,6 +354,7 @@ chatTypesDocsData = (sti @SrvError, STUnion, "SrvErr", [], "", ""), (sti @StoreError, STUnion, "SE", [], "", ""), (sti @SubscriptionStatus, STUnion, "SS", [], "", ""), + (sti @SupportGroupPreference, STRecord, "", [], "", ""), (sti @SwitchPhase, STEnum, "SP", [], "", ""), (sti @TimedMessagesGroupPreference, STRecord, "", [], "", ""), (sti @TimedMessagesPreference, STRecord, "", [], "", ""), @@ -366,11 +374,11 @@ chatTypesDocsData = (sti @UserPwdHash, STRecord, "", [], "", ""), (sti @XFTPErrorType, STUnion, "", [], "", ""), (sti @XFTPRcvFile, STRecord, "", [], "", ""), - (sti @XFTPSndFile, STRecord, "", [], "", "") + (sti @XFTPSndFile, STRecord, "", [], "", ""), -- (sti @DatabaseError, STUnion, "DB", [], "", ""), -- (sti @ChatItemInfo, STRecord, "", [], "", ""), -- (sti @ChatItemVersion, STRecord, "", [], "", ""), - -- (sti @ChatListQuery, STUnion, "CLQ", [], "", ""), + (sti @ChatListQuery, STUnion, "CLQ", [], "", ""), -- (sti @ChatName, STRecord, "", [], "", ""), -- (sti @ChatPagination, STRecord, "CP", [], "", ""), -- (sti @ConnectionStats, STRecord, "", [], "", ""), @@ -379,7 +387,10 @@ chatTypesDocsData = -- (sti @MemberReaction, STRecord, "", [], "", ""), -- (sti @MsgContentTag, (STEnum' $ dropPfxSfx "MC" '_'), "", ["MCUnknown_"], "", ""), -- (sti @NavigationInfo, STRecord, "", [], "", ""), - -- (sti @PaginationByTime, STRecord, "", [], "", ""), + -- PTAfter / PTBefore are hidden — bots only need "tail last N chats". + -- The wire format is parsed by paginationByTimeP in + -- src/Simplex/Chat/Library/Commands.hs. + (sti @PaginationByTime, STUnion1, "PT", ["PTAfter", "PTBefore"], "count=" <> Param "count", "") -- (sti @RcvQueueInfo, STRecord, "", [], "", ""), -- (sti @RcvSwitchStatus, STEnum, "", [], "", ""), -- incorrect -- (sti @SendRef, STRecord, "", [], "", ""), @@ -400,6 +411,7 @@ deriving instance Generic (CIReaction c d) deriving instance Generic (ContactUserPref p) deriving instance Generic (ContactUserPreference p) deriving instance Generic (CreatedConnLink m) +deriving instance Generic AddRelayResult deriving instance Generic AddressSettings deriving instance Generic AgentCryptoError deriving instance Generic AgentErrorType @@ -435,6 +447,7 @@ deriving instance Generic ClientNotice deriving instance Generic Color deriving instance Generic CommandError deriving instance Generic CommandErrorType +deriving instance Generic CommentsGroupPreference deriving instance Generic ComposedMessage deriving instance Generic Connection deriving instance Generic ConnectionEntity @@ -450,6 +463,7 @@ deriving instance Generic ContactStatus deriving instance Generic ContactUserPreferences deriving instance Generic CryptoFile deriving instance Generic CryptoFileArgs +deriving instance Generic DroppedMsg deriving instance Generic E2EInfo deriving instance Generic ErrorType deriving instance Generic FeatureAllowed @@ -473,6 +487,7 @@ deriving instance Generic GroupInfo deriving instance Generic GroupKeys deriving instance Generic GroupRootKey deriving instance Generic GroupLink +deriving instance Generic GroupLinkOwner deriving instance Generic GroupLinkPlan deriving instance Generic GroupMember deriving instance Generic GroupMemberAdmission @@ -501,6 +516,7 @@ deriving instance Generic JSONCIDirection deriving instance Generic JSONCIFileStatus deriving instance Generic JSONCIStatus deriving instance Generic LinkContent +deriving instance Generic LinkOwnerSig deriving instance Generic LinkPreview deriving instance Generic LocalProfile deriving instance Generic MemberCriteria @@ -516,6 +532,7 @@ deriving instance Generic MsgSigStatus deriving instance Generic NetworkError deriving instance Generic NewUser deriving instance Generic NoteFolder +deriving instance Generic OwnerVerification deriving instance Generic PendingContactConnection deriving instance Generic PrefEnabled deriving instance Generic Preferences @@ -535,6 +552,7 @@ deriving instance Generic RcvFileDescr deriving instance Generic RcvFileStatus deriving instance Generic RcvFileTransfer deriving instance Generic RcvGroupEvent +deriving instance Generic RcvMsgError deriving instance Generic RelayProfile deriving instance Generic RelayStatus deriving instance Generic ReportReason @@ -549,6 +567,7 @@ deriving instance Generic SndGroupEvent deriving instance Generic SrvError deriving instance Generic StoreError deriving instance Generic SubscriptionStatus +deriving instance Generic SupportGroupPreference deriving instance Generic SwitchPhase deriving instance Generic TimedMessagesGroupPreference deriving instance Generic TimedMessagesPreference @@ -573,7 +592,7 @@ deriving instance Generic XFTPSndFile -- deriving instance Generic DatabaseError -- deriving instance Generic ChatItemInfo -- deriving instance Generic ChatItemVersion --- deriving instance Generic ChatListQuery +deriving instance Generic ChatListQuery -- deriving instance Generic ChatName -- deriving instance Generic ChatPagination -- deriving instance Generic ConnectionStats @@ -583,7 +602,7 @@ deriving instance Generic XFTPSndFile -- deriving instance Generic MemberReaction -- deriving instance Generic MsgContentTag -- deriving instance Generic NavigationInfo --- deriving instance Generic PaginationByTime +deriving instance Generic PaginationByTime -- deriving instance Generic RcvQueueInfo -- deriving instance Generic RcvSwitchStatus -- deriving instance Generic SendRef diff --git a/bots/src/API/TypeInfo.hs b/bots/src/API/TypeInfo.hs index 37f74e4275..36e87db62d 100644 --- a/bots/src/API/TypeInfo.hs +++ b/bots/src/API/TypeInfo.hs @@ -214,6 +214,7 @@ toTypeInfo tr = "ProtocolServer", "SbKey", "SharedMsgId", + "Signature", "TransportHost", "UIColor", "UserPwd", diff --git a/cabal.project b/cabal.project index 12250de34b..0318f6ea42 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 99f9de71e5df213bb062fa11dd165778fc1d7160 + tag: 8bd3193280da6b4decf790bb57b470780c2576ba source-repository-package type: git diff --git a/docs/ANDROID.md b/docs/ANDROID.md index 61f81d1a40..d0422e1abd 100644 --- a/docs/ANDROID.md +++ b/docs/ANDROID.md @@ -49,7 +49,7 @@ Please, note, that if you use a modern version of SimpleX, the databases will be In order to view database data you need to decrypt it first. Install `sqlcipher` using your favorite package manager and run the following commands in the directory with databases: ```bash sqlcipher files_chat.db -pragma key="youDecryptionPassphrase"; +pragma key="yourDecryptionPassphrase"; # Ensure it works fine select * from users; ``` diff --git a/docs/BUSINESS.md b/docs/BUSINESS.md index 8fd5df5c36..b72bf00257 100755 --- a/docs/BUSINESS.md +++ b/docs/BUSINESS.md @@ -9,7 +9,7 @@ SimpleX Chat (aka SimpleX) is a decentralized communication network that provide This document aims to help you make the best use of SimpleX Chat if you choose to engage with its users. -## Communcate with customers via business address +## Communicate with customers via business address In the same way you can connect to our "SimpleX Chat team" profile via the app, you can provide the address for your existing and prospective customers: - to buy your product and services via chat, @@ -85,7 +85,7 @@ To install SimpleX Chat CLI in the cloud, follow this: simplex-chat ``` -To deattach from running CLI simply press `Ctrl+B` and then `D`. +To detach from a running CLI, simply press `Ctrl+B` and then `D`. To reattach back to CLI, run: `tmux attach -t simplex-cli`. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 6ae5418d0a..3ecfa17409 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -21,7 +21,7 @@ Please discuss the problem you want to solve and your detailed implementation pl ./contributing/CODE.md has details about general requirements common for `simplexmq` and `simplex-chat` repositories. -This files can be used with LLM prompts, e.g. if you use Claude Code you can create CLAUDE.md file in project root importing content from these files: +These files can be used with LLM prompts, e.g. if you use Claude Code you can create CLAUDE.md file in project root importing content from these files: ```markdown @README.md @@ -71,7 +71,7 @@ You will have to add `/opt/homebrew/opt/openssl@3.0/bin` to your PATH in order t 1. Make PRs to `master` branch _only_ for both simplex-chat and simplexmq repos. -2. To build core libraries for Android, iOS and windows: +2. To build core libraries for Android, iOS and Windows: - merge `master` branch to `master-android` branch. - push to GitHub. diff --git a/docs/DIRECTORY.md b/docs/DIRECTORY.md index 8659222280..50e7771a1a 100644 --- a/docs/DIRECTORY.md +++ b/docs/DIRECTORY.md @@ -96,7 +96,7 @@ Group owners are expected to moderate the content in the groups, if members post We reserve the right to not accept the group listing in the directory or cancel its listing, and there may be cases when we can't provide an explanation. We will certainly try to avoid it by communicating with the group owners first. -The combination of display name and full name has to be unique for the listed groups. If a group uses the name or logo of SimpleX, SimpleX network or SimpleX Chat it must be consistent with [Permitted Uses or SimpleX trademark](./TRADEMARK.md). +The combination of display name and full name has to be unique for the listed groups. If a group uses the name or logo of SimpleX, SimpleX network or SimpleX Chat it must be consistent with [Permitted Uses of SimpleX trademark](./TRADEMARK.md). Once the group is listed in the directory, the bot will invite you to join the group of the group owners, where you can send any ideas or suggestions for how the groups functionality should evolve, and help steer both the product and the policies. diff --git a/docs/DONATIONS.md b/docs/DONATIONS.md index 50eb62a401..f7a6e61d7b 100644 --- a/docs/DONATIONS.md +++ b/docs/DONATIONS.md @@ -7,10 +7,12 @@ permalink: /donate/index.html Huge thank you to everybody who donated to SimpleX Chat! -We are prioritizing users privacy and security - it would be impossible without your support. +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. +To ensure network independence and neutrality, we are currently finalizing the launch of [SimpleX Network Consortium](https://simplexnetwork.org/) - an agreement between SimpleX Network Foundation that is being formed as 501.c3 non-profit and SimpleX Chat company. + Your donations help us raise more funds - any amount, even the price of the cup of coffee, makes a big difference for us. Please donate via: @@ -25,6 +27,6 @@ Thank you, Evgeny, SimpleX Chat founder -## SimpleX Community Vouchers +## SimpleX Community Credits -Please comment on our plan to make SimpleX network sustainable and get a free access pass (an NFT) for early testing: https://simplex.chat/vouchers +Please comment on our plan to make SimpleX network sustainable: https://simplex.chat/credits diff --git a/docs/FAQ.md b/docs/FAQ.md index 401f025d9c..8c14168811 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -51,7 +51,7 @@ revision: 13.08.2025 ### How do I connect to people? -Tap "pencil" button in the right corner, then "Create 1-time link". Share the link with the person you want to connect to. Your contact has to paste the link to the app's search bar. The link will can also be opened via the browser, once the app is installed. +Tap "pencil" button in the right corner, then "Create 1-time link". Share the link with the person you want to connect to. Your contact has to paste the link to the app's search bar. The link can also be opened via the browser, once the app is installed. Alternatively, you can show the QR code when meeting in person or in a video call. @@ -103,7 +103,7 @@ Also see: [I do not see the second tick on the messages I sent](#i-do-not-see-th ### I want to see when my contacts read my messages -To know when your contact read your messages, your contact's app has to send you a confirmation message. And vice versa, for your contact to know when you read the message, your app has to send a confirmation message. +To know when your contact reads your messages, your contact's app has to send you a confirmation message. And vice versa, for your contact to know when you read the message, your app has to send a confirmation message. The important questions for this feature: - do you always want that your contacts can see when you read all their messages? Probably, even with your close friends, sometimes you would prefer to have time before you answer their message, and also have a plausible deniability that you have not seen the message. And this should be ok - in the end, this is your device, and it should be for you to decide whether this confirmation message is sent or not, and when it is sent. @@ -111,7 +111,7 @@ The important questions for this feature: Overall, it seems that this feature is more damaging to your communications with your contacts than it is helpful. It keeps senders longer in the app, nervously waiting for read receipts, exploiting addictive patterns - having you spend more time in the app is the reason why it is usually present in most messaging apps. It also creates a pressure on the recipients to reply sooner, and if read receipts are opt-in, it creates a pressure to enable it, that can be particularly damaging in any relationships with power imbalance. -We think that delivery receipts are important and equally benefit both sides as the conversation, as they confirm that communication network functions properly. But we strongly believe that read receipts is an anti-feature that only benefits the app developers, and hurts the relations between the app users. So we are not planning to add it even as opt-in. In case you want your contact to know you've read the message put a reaction to it. And if you don't want them to know it - it is also ok, what your device sends should be under your control. +We think that delivery receipts are important and equally benefit both sides as the conversation, as they confirm that communication network functions properly. But we strongly believe that read receipts are an anti-feature that only benefits the app developers, and hurts the relations between the app users. So we are not planning to add it even as opt-in. In case you want your contact to know you've read the message put a reaction to it. And if you don't want them to know it - it is also ok, what your device sends should be under your control. ### Can I use the same profile on desktop? Do messages sync cross-platform? @@ -130,7 +130,7 @@ We believe that allowing deleting information from your device to your contacts 2) it may be a business communication, and either your organization policy or a compliance requirement is that every message you receive must be preserved for some time. 3) the message can contain a legally binding promise, effectively a contract between you and your contact, in which case you both need to keep it. 4) the messages may contain threat or abuse and you may want to keep them as a proof. -5) you may have paid for the the message (e.g., it can be a design project or consulting report), and you don't want it to suddenly disappear before you had a chance to store it outside of the conversation. +5) you may have paid for the message (e.g., it can be a design project or consulting report), and you don't want it to suddenly disappear before you had a chance to store it outside of the conversation. It is also important to remember, that even if your contact enabled "Delete for everyone", you cannot really see it as a strong guarantee that the message will be deleted. Your contact's app can have a very simple modification (a one-line code change), that would prevent this deletion from happening when you request it. So you cannot see it as something that guarantees your security from your contacts. @@ -232,7 +232,7 @@ You may not have the second tick on your sent messages for these reasons: ### I see image preview but cannot open the image It can be for these reasons: -- your contact did not finish uploading the image file, possibly closing the app too quickly. When the image file is fully uploaded there will be a tick in the _top right corner_ or the image +- your contact did not finish uploading the image file, possibly closing the app too quickly. When the image file is fully uploaded there will be a tick in the _top right corner_ of the image. - your device fails to receive it. Please check server connectivity and run server tests, and also try increasing network timeouts in Advanced network settings. File reception was substantially improved in v5.7 - please make sure you are using the latest version. - file expired and can no longer be received. Files can be received only for 2 days after they were sent, after that they won't be available and will show X in the top right corner. @@ -298,7 +298,7 @@ You can resolve it by deleting the app's database: (WARNING: this results in del ### My mobile app does not connect to desktop app 1. Check that both devices are connected to the same network (e.g., it won't work if mobile is connected to mobile Internet and desktop to WiFi). -2. If you use VPN on mobile, allow connections to local network in you VPN settings, or disable VPN. +2. If you use VPN on mobile, allow connections to local network in your VPN settings, or disable VPN. 3. Allow SimpleX Chat on desktop to accept network connections in system firewall settings. You may choose a specific port in desktop app to accept connections, by default it uses a random port every time. 4. Check that your WiFi router allows connections between devices (e.g., it may have an option for "device isolation", or similar). 5. If you see an error "certificate expired", please check that your device clocks are synchronized within a few seconds. @@ -312,7 +312,7 @@ If none of the suggestions work for you, you can create a separate profile on ea ### Does SimpleX support post quantum cryptography? -Yes! Please read more about quantum resistant encryption is added to SimpleX Chat and about various properties of end-to-end encryption in [this post](../blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md). +Yes! Please read more about how quantum-resistant encryption is added to SimpleX Chat and about various properties of end-to-end encryption in [this post](../blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md). ### Why can't I use the same profile on different devices? @@ -355,7 +355,7 @@ If the servers didn't upgrade, the messages would temporarily fail to deliver. Y With private routing enabled, instead of connecting to your contact's server directly, your client would "instruct" one of the known servers to forward the message, preventing the destination server from observing your IP address. -Your messages are additionally end-to-end encrypted between your client and the destination server, so that the forwarding server cannot observe the destination addresses and server responses – similarly to how onion routing work. Private message routing is, effectively, a two-hop onion packet routing. +Your messages are additionally end-to-end encrypted between your client and the destination server, so that the forwarding server cannot observe the destination addresses and server responses – similarly to how onion routing works. Private message routing is, effectively, a two-hop onion packet routing. Also, this connection is protected from man-in-the-middle attack by the forwarding server, as your client will validate destination server certificate using its fingerprint in the server address. @@ -375,7 +375,7 @@ Private message routing routes packets (each message is one 16kb packet), not so As each message uses its own random encryption key and random (non-sequential) identifier, the destination server cannot link multiple message queue addresses to the same client. At the same time, the forwarding server cannot observe which (and how many) addresses on the destination server your client sends messages to, thanks to e2e encryption between the client and destination server. In that regard, this design is similar to onion routing, but with per-packet anonymity, not per-circuit. -This design is similar to mixnets (e.g. [Nym network](https://nymtech.net)), and it is tailored to the needs of message routing, providing better transport anonymity that general purpose networks, like Tor or VPN. You still can use Tor or VPN to connect to known servers, to protect your IP address from them. +This design is similar to mixnets (e.g. [Nym network](https://nymtech.net)), and it is tailored to the needs of message routing, providing better transport anonymity than general-purpose networks, like Tor or VPN. You still can use Tor or VPN to connect to known servers, to protect your IP address from them. ### Why don't you embed Tor in SimpleX Chat app? diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index fe0bd107ed..c8fdd56bdd 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -147,7 +147,7 @@ SimpleX Clients also form a network using SMP relays and IP or some other overla [Wikipedia](https://en.wikipedia.org/wiki/Overlay_network) -# Non-repudiation +## Non-repudiation The property of the cryptographic or communication system that allows the recipient of the message to prove to any third party that the sender identified by some cryptographic key sent the message. It is the opposite to [repudiation](#repudiation). While in some context non-repudiation may be desirable (e.g., for contractually binding messages), in the context of private communications it may be undesirable. @@ -157,7 +157,7 @@ The property of the cryptographic or communication system that allows the recipi Generalizing [the definition](https://csrc.nist.gov/glossary/term/pairwise_pseudonymous_identifier) from NIST Digital Identity Guidelines, it is an opaque unguessable identifier generated by a service used to access a resource by only one party. -In the context of SimpleX network, these are the identifiers generated by SMP relays to access anonymous messaging queues, with a separate identifier (and access credential) for each accessing party: recipient, sender and and optional notifications subscriber. The same approach is used by XFTP relays to access file chunks, with separate identifiers (and access credentials) for sender and each recipient. +In the context of SimpleX network, these are the identifiers generated by SMP relays to access anonymous messaging queues, with a separate identifier (and access credential) for each accessing party: recipient, sender and an optional notifications subscriber. The same approach is used by XFTP relays to access file chunks, with separate identifiers (and access credentials) for sender and each recipient. ## Peer-to-peer @@ -177,7 +177,7 @@ The quality of the end-to-end encryption scheme allowing to recover security aga ## Post-quantum cryptography -Any of the proposed cryptographic systems or algorithms that are thought to be secure against an attack by a quantum computer. It appears that as of 2025 there is no system or algorithm that is *proven* to be secure against such attacks, or even to be secure against attacks by massively parallel conventional computers, so a general recommendation is to use post-quantum hybrid cryptography - combining post-quantum and traditional algorigthms. +Any of the proposed cryptographic systems or algorithms that are thought to be secure against an attack by a quantum computer. It appears that as of 2025 there is no system or algorithm that is *proven* to be secure against such attacks, or even to be secure against attacks by massively parallel conventional computers, so a general recommendation is to use post-quantum hybrid cryptography - combining post-quantum and traditional algorithms. [Wikipedia](https://en.wikipedia.org/wiki/Post-quantum_cryptography) diff --git a/docs/REPRODUCE.md b/docs/REPRODUCE.md index 03fb6b4336..50c0b99280 100644 --- a/docs/REPRODUCE.md +++ b/docs/REPRODUCE.md @@ -46,7 +46,7 @@ echo -e "trust\n5\ny\nquit" | gpg --command-fd 0 --edit-key build@simplex.chat ## Verify release signature -**Linux dekstop apps and CLI**: +**Linux desktop apps and CLI**: Download the file with executable hashes and the signature. For example, to verify the `v6.5.0-beta.3` release: @@ -149,13 +149,13 @@ To reproduce the build you must have: The script executes these steps (please review the script to confirm): - 1) builds all Linux CLI and Dekstop binaries for the release in docker container. + 1) builds all Linux CLI and Desktop binaries for the release in docker container. 2) downloads binaries from the same GitHub release and compares them with the built binaries. 3) if they all match, generates _sha256sums file with their checksums. This will take a while. -4. After compilation, you should see the folder named as the tag and reprository name (e.g., `v6.4.8-simplex-chat`) with two subfolders: +4. After compilation, you should see the folder named as the tag and repository name (e.g., `v6.4.8-simplex-chat`) with two subfolders: ```sh ls v6.4.8-simplex-chat @@ -169,7 +169,7 @@ To reproduce the build you must have: ### Android apps -In addition to basic requirments, Android build will: +In addition to basic requirements, Android build will: - Take ~150gb of disc space - Take ~20h to build all the architectures (depends on core count) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 72db650c35..b9218fbebc 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -54,7 +54,7 @@ We will determine the risk of each issue, taking into account our experience dea **Issue severity levels** -- **CRITICAL severity**. Such issues should affect common configurations and be exploitable with low or medium difficulty. For example: significant disclosure of the encrypted users messages or files either via relays or via communication channels, vulnerabilities which can be easily exploited remotely to compromise clients or servers private keys. These issues will be kept private and will trigger a new release of all supported versions. +- **CRITICAL severity**. Such issues should affect common configurations and be exploitable with low or medium difficulty. For example: significant disclosure of the encrypted users' messages or files either via relays or via communication channels, vulnerabilities which can be easily exploited remotely to compromise clients or servers private keys. These issues will be kept private and will trigger a new release of all supported versions. - **HIGH severity**. This includes issues that are of a lower risk than critical, possibly due to affecting less common configurations, or have high difficulty to be exploited. These issues will be kept private and will trigger a new release of all supported versions. - **MEDIUM severity**. This includes issues like crashes in client applications caused by the received messages or files, flaws in protocols that are less commonly used, and local flaws. These will in general be kept private until the next release, and that release will be scheduled so that it can roll up several such flaws at one time. - **LOW severity**. This includes issues such as those that only affect the SimpleX CLI app, or unlikely configurations, or issues that would be classified as medium but are very difficult to exploit. These will in general be fixed immediately in latest development versions, and may be back-ported to older versions that are still getting updates. These issues may be kept private or be included in commit messages. diff --git a/docs/SERVER.md b/docs/SERVER.md index f45403be8a..a35ede5cd0 100644 --- a/docs/SERVER.md +++ b/docs/SERVER.md @@ -59,7 +59,7 @@ To create SMP server as a systemd service, you'll need: - Your server domain, with A and AAAA records specifying server IPv4 and IPv6 addresses (`smp1.example.com`) - A basic Linux knowledge. -*Please note*: while you can run an SMP server without a domain name, in the near future client applications will start using server domain name in the invitation links (instead of `simplex.chat` domain they use now). In case a server does not have domain name and server pages (see below), the clients will be generaing the links with `simplex:` scheme that cannot be opened in the browsers. +*Please note*: while you can run an SMP server without a domain name, in the near future client applications will start using server domain name in the invitation links (instead of `simplex.chat` domain they use now). In case a server does not have domain name and server pages (see below), the clients will be generating the links with `simplex:` scheme that cannot be opened in the browsers. 1. Install server with [Installation script](https://github.com/simplex-chat/simplexmq#using-installation-script). @@ -82,7 +82,7 @@ To create SMP server as a systemd service, you'll need: --control-port \ --socks-proxy \ --source-code \ - --fqdn=smp1.example.com + --fqdn=smp1.example.com' ``` 4. Install tor: @@ -114,7 +114,7 @@ To create SMP server as a systemd service, you'll need: ```sh # Enable log (otherwise, tor doesn't seem to deploy onion address) Log notice file /var/log/tor/notices.log - # Enable single hop routing (2 options below are dependencies of the third) - It will reduce the latency at the cost of lower anonimity of the server - as SMP-server onion address is used in the clients together with public address, this is ok. If you deploy SMP-server with onion-only address, keep standard configuration. + # Enable single hop routing (2 options below are dependencies of the third) - It will reduce the latency at the cost of lower anonymity of the server - as SMP-server onion address is used in the clients together with public address, this is ok. If you deploy SMP-server with onion-only address, keep standard configuration. SOCKSPort 0 HiddenServiceNonAnonymousMode 1 HiddenServiceSingleHopMode 1 @@ -194,12 +194,12 @@ To create SMP server as a systemd service, you'll need: key_name='web.key' cert_name='web.crt' - # Copy certifiacte from Caddy directory to smp-server directory + # Copy certificate from Caddy directory to smp-server directory cp "${folder_in}/${domain}.crt" "${folder_out}/${cert_name}" # Assign correct permissions chown "$user":"$group" "${folder_out}/${cert_name}" - # Copy certifiacte key from Caddy directory to smp-server directory + # Copy certificate key from Caddy directory to smp-server directory cp "${folder_in}/${domain}.key" "${folder_out}/${key_name}" # Assign correct permissions chown "$user":"$group" "${folder_out}/${key_name}" @@ -535,7 +535,7 @@ To verify server binaries after you downloaded them: > Good signature from "SimpleX Chat " -5. Compute the hashes of the binaries you plan to use with `shu256sum ` or with `openssl sha256 ` and compare them with the hashes in the file `_sha256sums` - they must be the same. +5. Compute the hashes of the binaries you plan to use with `sha256sum ` or with `openssl sha256 ` and compare them with the hashes in the file `_sha256sums` - they must be the same. That is it - you now verified authenticity of our GitHub server binaries. @@ -634,7 +634,7 @@ to initialize your `smp-server` configuration with: --- -After that, your installation is complete and you should see in your teminal output something like this: +After that, your installation is complete and you should see in your terminal output something like this: ```sh Certificate request self-signature ok @@ -742,7 +742,7 @@ websockets: off [PROXY] # Network configuration for SMP proxy client. # `host_mode` can be 'public' (default) or 'onion'. -# It defines prefferred hostname for destination servers with multiple hostnames. +# It defines preferred hostname for destination servers with multiple hostnames. # host_mode: public # required_host_mode: off @@ -757,7 +757,7 @@ websockets: off # or 'always' to be used for all destination hosts (can be used if it is an .onion server). # socks_mode: onion -# Limit number of threads a client can spawn to process proxy commands in parrallel. +# Limit number of threads a client can spawn to process proxy commands in parallel. # client_concurrency: 32 [INACTIVE_CLIENTS] @@ -823,7 +823,7 @@ Follow the steps to secure your CA keys: /etc/opt/simplex/ca.key ``` -3. Delete the CA key from the server. **Please make sure you've saved you CA key somewhere safe. Otherwise, you would lose the ability to [rotate the online certificate](#online-certificate-rotation)**: +3. Delete the CA key from the server. **Please make sure you've saved your CA key somewhere safe. Otherwise, you would lose the ability to [rotate the online certificate](#online-certificate-rotation)**: ```sh rm /etc/opt/simplex/ca.key @@ -913,9 +913,9 @@ SMP-server can also be deployed to be available via [Tor](https://www.torproject 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. + We're assuming you're using Ubuntu/Debian based distributions. If not, please refer to [official tor documentation](https://community.torproject.org/onion-services/setup/install/) or your distribution guide. - - Configure offical Tor PPA repository: + - Configure official Tor PPA repository: ```sh CODENAME="$(lsb_release -c | awk '{print $2}')" @@ -951,12 +951,12 @@ SMP-server can also be deployed to be available via [Tor](https://www.torproject 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. + And insert the following lines to the bottom of configuration. Please note lines starting with `#`: these are comments about each individual option. ```sh # Enable log (otherwise, tor doesn't seem to deploy onion address) Log notice file /var/log/tor/notices.log - # Enable single hop routing (2 options below are dependencies of the third) - It will reduce the latency at the cost of lower anonimity of the server - as SMP-server onion address is used in the clients together with public address, this is ok. If you deploy SMP-server with onion-only address, you may want to keep standard configuration instead. + # Enable single hop routing (2 options below are dependencies of the third) - It will reduce the latency at the cost of lower anonymity of the server - as SMP-server onion address is used in the clients together with public address, this is ok. If you deploy SMP-server with onion-only address, you may want to keep standard configuration instead. SOCKSPort 0 HiddenServiceNonAnonymousMode 1 HiddenServiceSingleHopMode 1 @@ -974,7 +974,7 @@ SMP-server can also be deployed to be available via [Tor](https://www.torproject 3. Start tor: - Enable `systemd` service and start tor. Offical `tor` is a bit flaky on the first start and may not create onion host address, so we're restarting it just in case. + Enable `systemd` service and start tor. Official `tor` is a bit flaky on the first start and may not create onion host address, so we're restarting it just in case. ```sh systemctl enable --now tor && systemctl restart tor @@ -994,7 +994,7 @@ SMP-server versions starting from `v5.8.0-beta.0` can be configured to PROXY smp 1. Install tor as described in the [previous section](#installation-for-onion-address). -2. Execute the following command to creatae a new Tor daemon instance: +2. Execute the following command to create a new Tor daemon instance: ```sh tor-instance-create tor2 @@ -1101,7 +1101,7 @@ _Please note:_ this configuration is supported since `v6.1.0-beta.2`. hosting_country: ``` -2. Install the webserver. For easy deployment we'll describe the installtion process of [Caddy](https://caddyserver.com) webserver on Ubuntu server: +2. Install the webserver. For easy deployment we'll describe the installation process of [Caddy](https://caddyserver.com) webserver on Ubuntu server: 1. Install the packages: @@ -1127,7 +1127,7 @@ _Please note:_ this configuration is supported since `v6.1.0-beta.2`. sudo apt update && sudo apt install caddy ``` - [Full Caddy instllation instructions](https://caddyserver.com/docs/install) + [Full Caddy installation instructions](https://caddyserver.com/docs/install) 3. Replace Caddy configuration with the following: @@ -1176,12 +1176,12 @@ _Please note:_ this configuration is supported since `v6.1.0-beta.2`. key_name='web.key' cert_name='web.crt' - # Copy certifiacte from Caddy directory to smp-server directory + # Copy certificate from Caddy directory to smp-server directory cp "${folder_in}/${domain}.crt" "${folder_out}/${cert_name}" # Assign correct permissions chown "$user":"$group" "${folder_out}/${cert_name}" - # Copy certifiacte key from Caddy directory to smp-server directory + # Copy certificate key from Caddy directory to smp-server directory cp "${folder_in}/${domain}.key" "${folder_out}/${key_name}" # Assign correct permissions chown "$user":"$group" "${folder_out}/${key_name}" @@ -1237,7 +1237,7 @@ smp://[:]@[,] - **optional** `` - Your configured password of `smp-server`. You can check your configured pasword in `/etc/opt/simplex/smp-server.ini`, under `[AUTH]` section in `create_password:` field. + Your configured password of `smp-server`. You can check your configured password in `/etc/opt/simplex/smp-server.ini`, under `[AUTH]` section in `create_password:` field. - ``, **optional** `` @@ -1368,9 +1368,9 @@ Here's the full list of commands, their descriptions and who can access them. | `stats-rts` | GHC/Haskell statistics. Can be enabled with `+RTS -T -RTS` option | - | | `clients` | Clients information. Useful for debugging. | yes | | `sockets` | General sockets information. | - | -| `socket-threads` | Thread infomation per socket. Useful for debugging. | yes | +| `socket-threads` | Thread information per socket. Useful for debugging. | yes | | `threads` | Threads information. Useful for debugging. | yes | -| `server-info` | Aggregated server infomation. | - | +| `server-info` | Aggregated server information. | - | | `delete` | Delete known queue. Useful for content moderation. | - | | `save` | Save queues/messages from memory. | yes | | `help` | Help menu. | - | @@ -1417,31 +1417,31 @@ fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,m | 20 | `pRelays_pRequests` | - requests | | 21 | `pRelays_pSuccesses` | - successes | | 22 | `pRelays_pErrorsConnect` | - connection errors | -| 23 | `pRelays_pErrorsCompat` | - compatability errors | +| 23 | `pRelays_pErrorsCompat` | - compatibility errors | | 24 | `pRelays_pErrorsOther` | - other errors | | Requested sessions with own relays: | | 25 | `pRelaysOwn_pRequests` | - requests | | 26 | `pRelaysOwn_pSuccesses` | - successes | | 27 | `pRelaysOwn_pErrorsConnect` | - connection errors | -| 28 | `pRelaysOwn_pErrorsCompat` | - compatability errors | +| 28 | `pRelaysOwn_pErrorsCompat` | - compatibility errors | | 29 | `pRelaysOwn_pErrorsOther` | - other errors | | Message forwards to all relays: | | 30 | `pMsgFwds_pRequests` | - requests | | 31 | `pMsgFwds_pSuccesses` | - successes | | 32 | `pMsgFwds_pErrorsConnect` | - connection errors | -| 33 | `pMsgFwds_pErrorsCompat` | - compatability errors | +| 33 | `pMsgFwds_pErrorsCompat` | - compatibility errors | | 34 | `pMsgFwds_pErrorsOther` | - other errors | | Message forward to own relays: | | 35 | `pMsgFwdsOwn_pRequests` | - requests | | 36 | `pMsgFwdsOwn_pSuccesses` | - successes | | 37 | `pMsgFwdsOwn_pErrorsConnect` | - connection errors | -| 38 | `pMsgFwdsOwn_pErrorsCompat` | - compatability errors | +| 38 | `pMsgFwdsOwn_pErrorsCompat` | - compatibility errors | | 39 | `pMsgFwdsOwn_pErrorsOther` | - other errors | | Received message forwards: | | 40 | `pMsgFwdsRecv` | | -| Message queue subscribtion errors: | +| Message queue subscription errors: | | 41 | `qSub` | All | -| 42 | `qSubAuth` | Authentication erorrs | +| 42 | `qSubAuth` | Authentication errors | | 43 | `qSubDuplicate` | Duplicate SUB errors | | 44 | `qSubProhibited` | Prohibited SUB errors | | Message errors: | @@ -1526,9 +1526,9 @@ To update your smp-server to latest version, choose your installation method and sudo systemctl start smp-server ``` - - [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) + - [Official installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) - 1. Execute the followin command: + 1. Execute the following command: ```sh sudo simplex-servers-update @@ -1640,7 +1640,7 @@ To reproduce the build you must have: ## Configuring the app to use the server -To configure the app to use your messaging server copy it's full address, including password, and add it to the app. You have an option to use your server together with preset servers or without them - you can remove or disable them. +To configure the app to use your messaging server copy its full address, including password, and add it to the app. You have an option to use your server together with preset servers or without them - you can remove or disable them. It is also possible to share the address of your server with your friends by letting them scan QR code from server settings - it will include server password, so they will be able to receive messages via your server as well. diff --git a/docs/SIMPLEX.md b/docs/SIMPLEX.md index ec25afaf88..e24275d656 100644 --- a/docs/SIMPLEX.md +++ b/docs/SIMPLEX.md @@ -89,7 +89,7 @@ There are several P2P chat/messaging protocols and implementations that aim to s 5. All known P2P networks are likely to be vulnerable to [Sybil attack][12], because each node is discoverable, and the network operates as a whole. Known measures to reduce the probability of the Sybil attack either require a centralized component or expensive [proof of work][13]. The proposed design, on the opposite, has no server discoverability - servers are not connected, not known to each other and to all clients. The SimpleX network is fragmented and operates as multiple isolated connections. It makes network-wide attacks on SimpleX network impossible - even if some servers are compromised, other parts of the network can operate normally, and affected clients can switch to using other servers without losing contacts or messages. -6. P2P networks are likely to be [vulnerable][14] to [DRDoS attack][15]. In the proposed design clients only relay traffic from known trusted connection and cannot be used to reflect and amplify the traffic in the whole network. +6. P2P networks are likely to be [vulnerable][14] to [DRDoS attack][15]. In the proposed design clients only relay traffic from known trusted connections and cannot be used to reflect and amplify the traffic in the whole network. [1]: https://en.wikipedia.org/wiki/End-to-end_encryption [2]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack diff --git a/docs/TRANSLATIONS.md b/docs/TRANSLATIONS.md index 2b1febb6f2..a0250b6ab2 100644 --- a/docs/TRANSLATIONS.md +++ b/docs/TRANSLATIONS.md @@ -35,7 +35,7 @@ The steps are: ### Translating Android app -1. Please start from [Android app](https://hosted.weblate.org/projects/simplex-chat/android/), both when you do the most time-consuming initial translation, and add any strings later. Firstly, iOS strings can be a bit delayed from appearing in Weblate, as it requires a manual step from us before they are visible. Secondary, Android app is set up as a glossary for iOS app, and 2/3 of all strings require just to clicks to transfer them from Android to iOS (it still takes some time, Weblate doesn't automate it, unfortunately). +1. Please start from [Android app](https://hosted.weblate.org/projects/simplex-chat/android/), both when you do the most time-consuming initial translation, and add any strings later. Firstly, iOS strings can be a bit delayed from appearing in Weblate, as it requires a manual step from us before they are visible. Secondly, Android app is set up as a glossary for iOS app, and 2/3 of all strings require just two clicks to transfer them from Android to iOS (it still takes some time, Weblate doesn't automate it, unfortunately). 2. Some of the strings do not need translations, but they still need to be copied over - there is a button in weblate UI for that: diff --git a/docs/WEBRTC.md b/docs/WEBRTC.md index a48cd12b00..b4862e0d5b 100644 --- a/docs/WEBRTC.md +++ b/docs/WEBRTC.md @@ -18,7 +18,7 @@ For this guide, we'll be using the most featureful and battle-tested STUN/TURN s 1. Install `coturn` package from the main repository. ```sh -apt update && apt install coturn` +apt update && apt install coturn ``` 2. Uncomment `TURNSERVER_ENABLED=1` from `/etc/default/coturn`: @@ -44,7 +44,7 @@ user=$YOUR_LOGIN:$YOUR_PASSWORD server-name=$YOUR_DOMAIN # The default realm to be used for the users when no explicit origin/realm relationship was found realm=$YOUR_DOMAIN -# Path to your certificates. Make sure they're readable by cotun process user/group +# Path to your certificates. Make sure they're readable by coturn process user/group cert=/var/lib/turn/cert.pem pkey=/var/lib/turn/key.pem # Use 2066 bits predefined DH TLS key @@ -97,7 +97,7 @@ To configure your mobile app to use your server: 1. Open `Settings / Network & Servers / WebRTC ICE servers` and switch toggle `Configure ICE servers`. -2. Enter all server addresses in the field, one per line, for example if you servers are on the port 5349: +2. Enter all server addresses in the field, one per line, for example if your servers are on the port 5349: ``` stun:stun.example.com:5349 @@ -116,7 +116,7 @@ This is it - you now can make audio and video calls via your own server, without ping ``` - If packets being transmitted, server is up! + If packets are being transmitted, the server is up! - **Determine if ports are open**: diff --git a/docs/XFTP-SERVER.md b/docs/XFTP-SERVER.md index ba4770644e..43edbdda7f 100644 --- a/docs/XFTP-SERVER.md +++ b/docs/XFTP-SERVER.md @@ -9,7 +9,7 @@ revision: 31.07.2023 - [Overview](#overview) - [Installation options](#installation-options) - [systemd service](#systemd-service) with [installation script](#installation-script) or [manually](#manual-deployment) - - [docker container](#docker-сontainer) + - [docker container](#docker-container) - [Linode marketplace](#linode-marketplace) - [Tor installation](#tor-installation) - [Configuration](#configuration) @@ -72,7 +72,7 @@ Manual installation is the most advanced deployment that provides the most flexi 1. Install binary: - - Using offical binaries: + - Using official 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 && chmod +x /usr/local/bin/xftp-server @@ -129,9 +129,9 @@ Manual installation is the most advanced deployment that provides the most flexi And execute `sudo systemctl daemon-reload`. -### Docker сontainer +### Docker container -You can deploy smp-server using Docker Compose. This is second recommended option due to its popularity and relatively easy deployment. +You can deploy xftp-server using Docker Compose. This is the second recommended option due to its popularity and relatively easy deployment. This deployment provides two Docker Compose files: the **automatic** one and **manual**. If you're not sure, choose **automatic**. @@ -197,9 +197,9 @@ xftp-server can also be deployed to serve from [tor](https://www.torproject.org) 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. + We're assuming you're using Ubuntu/Debian based distributions. If not, please refer to [official tor documentation](https://community.torproject.org/onion-services/setup/install/) or your distribution guide. - - Configure offical Tor PPA repository: + - Configure official Tor PPA repository: ```sh CODENAME="$(lsb_release -c | awk '{print $2}')" @@ -235,10 +235,10 @@ xftp-server can also be deployed to serve from [tor](https://www.torproject.org) 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. + And insert the following lines to the bottom of configuration. Please note lines starting with `#`: these are comments about each individual option. ```sh - # Enable log (otherwise, tor doesn't seemd to deploy onion address) + # Enable log (otherwise, tor doesn't seem 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 @@ -257,7 +257,7 @@ xftp-server can also be deployed to serve from [tor](https://www.torproject.org) 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. + Enable `systemd` service and start tor. Official `tor` is a bit flaky 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 @@ -356,7 +356,7 @@ To password-protect your `xftp-server`, change it in the configuration: ``` --- -After that, your installation is complete and you should see in your teminal output something like this: +After that, your installation is complete and you should see in your terminal output something like this: ```sh Certificate request self-signature ok @@ -398,7 +398,7 @@ xftp://[:]@[,] - **optional** `` - Your configured password of `xftp-server`. You can check your configured pasword in `/etc/opt/simplex-xftp/file-server.ini`, under `[AUTH]` section in `create_password:` field. + Your configured password of `xftp-server`. You can check your configured password in `/etc/opt/simplex-xftp/file-server.ini`, under `[AUTH]` section in `create_password:` field. - ``, **optional** `` @@ -609,8 +609,8 @@ To update your XFTP server to latest version, choose your installation method an sudo systemctl start xftp-server ``` - - [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) - 1. Execute the followin command: + - [Official installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) + 1. Execute the following command: ```sh sudo simplex-servers-update ``` diff --git a/docs/protocol/channels-overview.md b/docs/protocol/channels-overview.md new file mode 100644 index 0000000000..d4cd2d2965 --- /dev/null +++ b/docs/protocol/channels-overview.md @@ -0,0 +1,341 @@ +Revision 1, 2026-04-28 + +# SimpleX Channels: stateful information delivery and management + +## Table of contents + +- [Introduction](#introduction) + - [What are SimpleX Channels](#what-are-simplex-channels) + - [Channels as transport layer](#channels-as-transport-layer) + - [Content visibility and participant privacy](#content-visibility-and-participant-privacy) + - [In comparison](#in-comparison) + - [Non-goals](#non-goals) +- [Architecture](#architecture) + - [State and distribution](#state-and-distribution) + - [Identity and ownership](#identity-and-ownership) + - [Governance](#governance) + - [Roles](#roles) +- [Cryptographic primitives](#cryptographic-primitives) +- [Security](#security) + - [Design objectives](#design-objectives) + - [Signing scope: roster only, content optional](#signing-scope-roster-only-content-optional) + - [Threat model](#threat-model) + - [Current gaps](#current-gaps) +- [Future work](#future-work) + - [Stateful access and history navigation](#stateful-access-and-history-navigation) + - [Transcript integrity](#transcript-integrity) + - [End-to-end encrypted side conversations](#end-to-end-encrypted-side-conversations) + - [Relay addition and removal](#relay-addition-and-removal) + - [Governance evolution](#governance-evolution) + - [Pre-moderation](#pre-moderation) + - [Scheduled delivery](#scheduled-delivery) + - [Link preview proxying](#link-preview-proxying) +- [Conclusion](#conclusion) + + +## Introduction + +The SimpleX network provides private point-to-point communication without user or endpoint identifiers, but most speech that matters is public. Every existing platform that distributes content at scale identifies both publishers and their audiences to the operator - none protect participation privacy. SimpleX Chat supported peer-to-peer groups, but they cannot scale to large audiences. SimpleX Channels close this gap. + +### What are SimpleX Channels + +SimpleX Channels are a stateful information delivery and management layer built on the [SimpleX network](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md). SMP queues provide stateless, unidirectional packet delivery between two endpoints. Channels add persistence, state, and scalable distribution - enabling one-to-many publishing with cryptographic identity independent of infrastructure operators. + +[SimpleX Chat](https://simplex.chat) is the first application, presenting channels as a broadcast publication model where owners publish and subscribers read, react, and comment. But channels are not limited to this use case - they are a general-purpose layer for distributing and managing stateful information (feeds, telemetry, automated pipelines, coordination services, social media). This document describes channels as a transport mechanism - the same mechanism will also be used for large groups, communities, wikis, forums, and other social media primitives. + +The critical difference from conventional publish-subscribe systems is that channel identity and governance are controlled cryptographically by the channel owners, not by the infrastructure operators. Relays - SimpleX network clients that forward and optionally cache channel content - can be added, removed, and replaced without changing the channel's identity, address, content, or cryptographic trust chain. A channel's relationship with its relays is transient; its identity is permanent. The authoritative record of content is hosted on channel owners' devices; relays perform transmission and caching similar to CDN infrastructure. + +Channel owners hold full control of the channel - its identity, content, governance rules, and membership - through self-custody of cryptographic keys. No infrastructure operator, relay provider, or third party can control or alter a channel without the owner's keys. Blockchain systems achieve a related property for financial assets - no third party can control holdings - through network-wide consensus. Channels achieve it through local authority and cryptographic signatures, without global consensus or a public ledger. Unlike blockchain state, channel state is mutable by the owner and not publicly verifiable by third parties. + +### Channels as transport layer + +The SimpleX network has three transport layers, each built on the one below: + +1. **SMP** ([SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md)) - stateless, unidirectional packet delivery between two endpoints through SMP routers. Provides fixed-size blocks, 2-node onion routing, and transport metadata protection. + +2. **SimpleX agents** ([agent protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/agent-protocol.md)) - bidirectional, redundant connections between endpoints, with end-to-end post-quantum double ratchet encryption. The [SimpleX Chat Protocol](./simplex-chat.md) runs on top of this layer, providing direct messaging, group communication, and metadata delivery for file transfers via [XFTP protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/xftp.md). + +3. **Channels** - stateful, one-to-many information delivery and management with cryptographic ownership and programmable governance. This layer runs on top of chat and agent layer 2, and it is described in this document. + +No network-wide user profile identifiers exist at any of these layers. Just as SMP enables private messaging by providing transport without user identifiers, channels enable public communication while preserving participation privacy at the distribution layer. + +Channel relays are themselves SimpleX clients in the SMP network, connecting to SMP routers using the same protocol, the same 2-node onion routing, and the same fixed-size transport blocks as any other endpoint. Even though the SMP network can distinguish a relay from a person's phone by its transport patterns, it prevents relays from learning anything about other network endpoints. In the case of SimpleX Chat, any CLI client can act as a chat relay without modifications. + +Channels therefore inherit all of SMP's transport privacy properties: + +- **Relays cannot observe subscriber network addresses.** The relay sees SMP queue addresses, not IP addresses or network sessions. The subscriber's IP is known only to their SMP router, which cannot see the message content (encrypted at the agent layer) or the IP addresses of whoever sends messages. + +- **SMP routers cannot see channel content.** Messages between relay and subscriber are end-to-end encrypted. The SMP router forwards fixed-size encrypted blocks without knowing whether they carry channel messages, direct messages, or anything else. + +- **Participation in multiple channels is unlinkable.** Each channel connection uses independent SMP queues with separate cryptographic credentials. Because of packet-level anonymity in 2-node routing, even if a subscriber uses the same SMP routers for all channels, the sending relays cannot determine this without collusion with those routers. Clients choose independently operated routers by default. + +No single point in the system sees both content and network identity. SMP routers see network addresses but not content, and no single SMP router can see which endpoints are communicating because clients choose independently operated routers. Relays see content but not network addresses. + +### Content visibility and participant privacy + +Any channel joinable via a public link, whether encrypted or not, must be considered completely public - the cost of joining through automated means has collapsed with large language models and is approaching zero. End-to-end encrypting such content provides no privacy; it only undermines users' security by creating false expectations and increases infrastructure operators' risks by making them unable to see what they deliver. Private channels with encrypted content are a separate use case discussed in [Future work](#end-to-end-encrypted-side-conversations). + +Content of public channels is therefore not end-to-end encrypted between owner and subscriber. Relays can read the messages they forward. Relay operators cannot undetectably alter channel content when multiple relays serve the channel, and cannot alter signed content at all - the authoritative state is held by owners. That each channel can use multiple chat relays provides both technical reliability and censorship resistance against any relay-specific content policies. + +The achievable privacy property for public communication is participation privacy - protecting who reads and writes content. The SMP transport carries no user identifiers, and relays are ordinary SMP clients, so subscribers connect without revealing their identity, network address, or any information that persists across channels. If an adversary joins a SimpleX channel, they see everything that is sent, but cannot determine who sent it or link any participant to anything outside the channel. + +Other systems make the opposite choice: content encryption in exchange for participant identification. For groups and channels joinable via public links this is the opposite of what is needed - the content encryption is meaningless (anyone can join and read), while the participant identification is the security threat. + +### In comparison + +**Telegram channels** - the operator controls channel identity (usernames are revocable), has full access to both content and participant identity. Channels cannot exist without Telegram's permission. + +**Nostr relays** - a single persistent key is used for publishing, following, and identity. Relays see content, the user's key, and their IP address. All posts and follow lists are signed and non-repudiable, linked to the same key - making both publishing and reading activity traceable and undeniable. + +**Signal groups** - content is end-to-end encrypted, but the operator manages group state and can observe the membership graph. Groups are capped at 1,000 members with no concept of a channel. + +**Matrix rooms** - server operators see room membership and metadata. Room identity is bound to the creating server's domain - if the server disappears, the room identity is lost. + +**Mastodon / ActivityPub** - publisher identity is bound to a server domain - if the server disappears, the identity is lost. Server operators see all content and all follower relationships. No encryption or privacy of any kind. + +| Property | Telegram | Nostr | Signal | Matrix | Mastodon | **SimpleX** | +|---|---|---|---|---|---|---| +| Content visible to operator | Yes | Yes | No | Configurable | Yes | **Yes** | +| Participant identity visible to operator | Yes | Yes | Yes | Yes | Yes | **No** | +| Channel identity independent of infrastructure | No | Yes | No | No | No | **Yes** | +| Sovereign ownership (no 3rd party can seize) | No | Yes | No | No | No | **Yes** | +| Programmable governance | No | No | No | No | No | **Planned** | +| Cryptographic content deniability | No | No | Yes | Yes | No | **Yes (default)** | +| Scalable one-to-many delivery | Yes | Yes | No | Limited | Yes | **Yes** | + +### Non-goals + +Channels do not attempt to: + +- **Encrypt public content from relay operators.** See [Content visibility and participant privacy](#content-visibility-and-participant-privacy). +- **Assign persistent identities to participants.** There are no usernames, public keys, or any identifiers that persist across channels or link activity across contexts. +- **Require network-wide consensus.** Channel state is authoritative on owner devices. The network does not validate channel transactions. +- **Guarantee immutability of content.** Channel state is fully controlled and mutable by owners, unlike blockchain state, which is immutable by design. + +## Architecture + +The introduction established what channels provide and why. This section describes how: where state lives, how identity and ownership work, how governance evolves, and what each participant does. + +### State and distribution + +The authoritative record of a channel - content, member roster, profile, cryptographic keys, governance rules - is held by channel owners on their own devices, not on relays, not on any server, and not on any shared ledger. Relays hold transient copies for distribution and optional caching, analogous to CDN edge nodes: the origin holds the truth, CDN nodes come and go. Consensus is only required between channel owners, not across the entire network. + +``` + ┌──────────┐ + │ Owner │ <- authoritative state + └────┬─────┘ + │ + ┌───────────┼───────────┐ + │ │ │ + ┌────▼───┐ ┌────▼───┐ ┌────▼───┐ + │Relay A │ │Relay B │ │Relay C │ <- cache / distribution + └────┬───┘ └────┬───┘ └────┬───┘ + │ │ │ + ┌─────┼─────┐ ... ┌─────┼─────┐ + │ │ │ │ │ │ + S1 S2 S3 S7 S8 S9 <- received copies +``` + +Content originates on the owner's device and flows through relays to subscribers. Each relay independently forwards to all of its subscribers. Subscribers do not connect to owners or to each other - this provides better scalability than peer-to-peer SimpleX groups, where adding a member requires N new connections. When multiple relays serve the same channel, subscribers deduplicate at the client level. + +**Failure modes:** + +- **Loss of a relay is loss of a cache node, not loss of data.** The owner can send the same content through a replacement relay. + +- **Loss of all owner devices is the catastrophic event** - relay caches become orphaned and the channel's private keys are gone. Multiple owners and backups mitigate this risk. + +- **Disagreements between relays are resolved by the origin.** The owner's version is authoritative, settling cache inconsistency through any reachable relay. + +Subscribers hold their own received copies. Signed messages are independently verifiable without consulting the relay or owner. Unsigned content depends on cross-relay consistency or future transcript integrity mechanisms. + +### Identity and ownership + +A channel's identity is the SHA-256 hash of the genesis root public key, computed at creation time and never changed - even if relays are added, removed, or the channel link is rotated. It is self-authenticating: derived from a key pair that only the channel creator held. It is embedded in the channel's link, distributed in the profile to all members, and used as a binding prefix in all signed messages. + +Subscribers validate that the identity in the link matches the identity in the profile, preventing link substitution. Profile updates that attempt to change the identity are rejected. Full validation that the identity equals the hash of the root key is deferred: if current clients enforced this check, they would reject future rotated links as invalid. The identity is correctly managed today; validation will be enforced with the key rotation protocol. See the [group identity binding RFC](../rfcs/2026-03-28-group-identity-binding.md). + +The root key does not sign messages directly. Instead, it authorizes owner keys through a signed chain. At creation, the owner generates a root key pair and a separate member key pair for signing. The member key is published as an authorization entry signed by the root key. New owners can be added by any previously authorized owner signing a new entry. Anyone retrieving the channel link can verify this chain without network access. + +The root key is a bootstrap key - it certifies owners, then need not be used again. All owners are cryptographically indistinguishable to subscribers (they all have equally valid authorization chains), which - provided multiple owners were signed by the root key - conceals the creator's identity. + +The channel link is the out-of-band trust anchor - relays and SMP routers cannot modify link content. All members announce their signing keys on joining. Owner keys are verifiable against the link. Role changes (promoting members to admin, moderator) are signed by owners at the protocol level. + +A planned extension will record role changes as a linearly ordered signed roster log with consistent sequencing across all owners, relays, and subscribers. This linearization prevents ambiguous roster states from concurrent unordered changes, and creates a verifiable chain of trust from the channel link through owners to all elevated roles. Out-of-band key verification for non-owner members will further extend this to E2E encrypted conversations. + +### Governance + +"Management" in "information delivery and management" refers not only to managing content but to managing the channel itself - who can make decisions, and how. + +The low-level protocol supports multiple owners from the initial release. The application-level governance model evolves through a planned progression: + +**Current (v6.5): Single owner.** One owner controls the channel. All administrative actions (profile changes, roster modifications, relay management) are decided by this single owner. The protocol-level owners chain supports verification of multiple entries, but the application creates and manages only one. + +**Near-term (v7): Multiple owners, any-owner-decides.** Multiple owners share control of the channel. Any owner can independently make any administrative decision - add or remove members, change the profile, manage relays. This is the most common decision-making model in practice (equivalent to "all admins are equal" in most online platforms). No coordination between owners is required for any action. + +**Future: Multisig and programmable governance.** Further stages include M-of-N multisig for administrative actions and, eventually, programmable governance rules defined as code in the channel's definition. The protocol must support these without prescribing a specific governance model. + +### Roles + +- **Owners** create the channel, hold the authoritative state and private keys on their devices, publish content, and manage the member roster. Owners sign administrative messages and optionally content messages. A channel must have at least one owner. + +- **Relays** receive content from owners and members with posting rights, optionally cache it, and forward it to subscribers. They accept new subscriber connections and introduce them to the channel owners. Relays cannot author messages. A channel must have at least one active relay. Relays are ordinary SimpleX clients - a relay can be operated by anyone (a channel operator, a third-party service provider, or a self-hosted instance) and each creates its own contact address link, bound to the channel's identity. The relay's relationship with the channel is transient - owners can add and remove relays without changing the channel's identity. + +- **Subscribers** connect to relays and receive content. They cannot send messages by default, but can be given posting rights. + +Additional roles (moderator, admin, member, author) exist in the hierarchy and are inherited from the group protocol. + +For protocol-level detail - wire formats, message types, signing and verification mechanics, delivery pipeline - see [SimpleX Channels Protocol](./channels-protocol.md). + + +## Cryptographic primitives + +- **Ed25519** - channel identity (root key pair), owner authorization chain, and message signing. The signature binding prefix includes the channel's entity ID and the sender's member ID, preventing cross-channel replay. + +- **SHA-256** - derives the channel's entity ID from the genesis root public key. Immutable, serves as the channel's permanent identity. + +- **Double ratchet with post-quantum KEM** (inherited from [SimpleX agent layer](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/agent-protocol.md)) - end-to-end encryption for all SMP transport. Not channel-specific - channels inherit it by being built on the agent layer. Future E2E side conversations (support scope, member DMs, private channels) will use the same mechanism. + +Content messages are not signed by default to preserve cryptographic deniability - see [Signing scope](#signing-scope-roster-only-content-optional). Owners may opt into signing all content in a future release. + + +## Security + +This section examines what the architectural properties protect against, where they hold, and where gaps remain. + +### Design objectives + +The channel protocol is designed to achieve the following security objectives: + +1. **Stable message delivery** between channel participants, resilient to individual relay failures. +2. **No possibility for a relay to substitute the channel** - the channel's identity is cryptographically bound to the link and profile controlled by channel owners. +3. **No possibility for a relay to impersonate an owner** - administrative messages require valid signatures. +4. **Prevention of relay-initiated roster manipulation** - member removal, role changes, and other roster modifications require valid owner signatures. +5. **Relay transience** - the owner can add and remove relays, including the last relay, without permanently losing the channel. Subscribers can restore connectivity by retrieving updated link data. +6. **Sender anonymity within multi-owner channels** - owners can publish as the channel, hiding which specific owner authored a message from subscribers. +7. **Participant privacy** - relay operators cannot determine subscriber identity or network address, and subscribers cannot determine each other's identity. This is inherited from the SMP transport layer. + +### Signing scope: roster only, content optional + +By default, only roster-modifying and administrative messages are signed. Content messages are not signed. Two reasons: + +1. **Cryptographic deniability.** Signing creates non-repudiable proof of authorship verifiable by any third party. Without signatures, no such proof exists - a relay could have fabricated any unsigned message. + +2. **Proportional defense.** Changes to roster, channel profile, and permissions can be disruptive and irreversible - they must be authenticated at processing time. Content manipulation is detectable post-hoc through cross-relay consistency, and the authoritative record on the owner's device is unaffected. + +Owners will be able to opt into signing content on a per-channel or per-message basis - some publishers want non-repudiable authorship, others prefer deniability. + +### Threat model + +This threat model assumes the [SimpleX network threat model](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/security.md) and addresses threats specific to the channel layer. + +**A single compromised relay** + +*can:* + +- Substitute unsigned content or selectively drop messages for its subscribers. Detectable by subscribers connected to other relays - the owner's version is authoritative. TODO: difference detection not yet implemented. +- Selectively target specific subscribers while delivering correctly to others. +- Ignore the "message from channel" directive, revealing which owner sent a message. Detectable out-of-band. +- Fabricate or hide subscriber connections, inflating or deflating counts. Detectable if subscribers are connected to other relays. + +*cannot:* + +- Undetectably substitute content - subscribers on honest relays receive the original. +- Alter the channel's authoritative state on the owner's device. +- Substitute the channel profile or impersonate an owner - these require valid signatures. +- Redirect subscribers to a different channel - the entity ID is validated across link and profile. +- Determine subscriber identity or network address - inherited from SMP transport. +- Correlate subscriber participation across channels - each connection uses independent SMP queues. The subscriber chooses their SMP router independently, so collusion between a relay and the relay's SMP router does not compromise connections through a different router. + +**All relays compromised and colluding** + +*can:* + +- Undetectably substitute unsigned content for all subscribers, unless owners sign content messages. +- Prevent delivery of any messages, including signed ones (signing prevents substitution, not dropping). +- Fabricate or hide subscriber connections undetectably. + +*cannot:* + +- Forge signed administrative messages or substitute the channel profile. +- Alter the authoritative state on the owner's device. + +**Compromise of owner keys** + +An attacker who obtains the root private key or an owner's member private key (through device compromise, backup theft, or coercion) can impersonate the owner and sign arbitrary administrative messages. This is a different threat from key loss - the channel continues operating, but under adversarial control. Mitigation depends on owner-side operational security and future multisig governance. For the threat model of the channel link itself (the trust anchor), see the [short links for groups RFC](https://github.com/simplex-chat/simplexmq/blob/stable/rfcs/2025-04-04-short-links-for-groups.md). + +**Loss of all owner devices** + +The channel can have no new content, no administrative updates, no new owners. Relay caches continue delivering existing content but cannot be refreshed, and will eventually expire in the absence of the owner connection. Multiple owners and key backups mitigate this risk. + +**A subscriber** + +*can:* + +- See all public content, by design. +- Join multiple times with different profiles, inflating counts. + +*cannot:* + +- Identify other subscribers, send messages to the channel (unless given posting rights), or forge messages of the owner or other subscribers. + +**A passive network observer** + +*can:* + +- Observe communication with an SMP router, but not whether it is channel-related. + +*cannot:* + +- Determine which channel a subscriber uses, correlate channel activity with other SimpleX activity, or identify a relay as distinct from an ordinary user, other than by traffic volume. Inherited from SMP transport. + +### Current gaps + +1. **Cross-relay consistency detection.** Duplicate messages are silently deduplicated without hash comparison. Designed but not implemented. +2. **Link entity ID validation.** Deferred to a future version with key rotation. See [group identity binding RFC](../rfcs/2026-03-28-group-identity-binding.md). +3. **Multi-relay UX.** Protocol supports multiple relays per subscriber; no UX for monitoring relay-level delivery health. It will be added in v6.5.x. + + +## Future work + +### Stateful access and history navigation + +Currently, relays send recent cached history on join but do not support navigation or search. Planned: history pagination by timestamp or message ID, remote search against relay caches, and selective retrieval of specific message ranges. Relay operators can differentiate on cache depth and search capabilities. + +### Transcript integrity + +- **Opt-in content signing.** Per-channel or per-message choice to sign content, making it non-repudiable. This will be released in SimpleX Chat v7. +- **Subscriber transcript acknowledgment.** Subscribers periodically sign a digest of received history ("I've seen it" rather than "I've authored it"), enabling detection of relay manipulation through diverging digests. +- **Merkle tree signing.** Owner periodically publishes a signed Merkle root. Subscribers verify their copies against the owner's authoritative record. + +### End-to-end encrypted side conversations + +- **E2E encrypted support scope** between subscriber and moderator/owner. +- **E2E encrypted DMs between members** where channel settings permit, using standard SimpleX connection establishment. +- **Private channels** where the entire content stream is encrypted to authorized subscribers. The relay becomes a conduit that sees neither content nor identity. + +### Relay addition and removal + +Dynamic relay addition with cache population from existing relays or owner. Relay removal with subscriber migration. Relay rotation with continuity - new relay connects before old relay is removed. It will be added in v6.5.x. + +### Governance evolution + +- **Multiple owners (v7):** concurrent administrative authority, any owner acts independently. +- **Multisig:** M-of-N approval for administrative actions, with per-action quorums. +- **Programmable governance:** rules defined as code in the channel definition. + +### Pre-moderation + +Subscriber messages reviewed by moderators before becoming visible to all subscribers. + +### Scheduled delivery + +Messages scheduled for future delivery, cached by relay until the scheduled time. + +### Link preview proxying + +The relay loads link previews on behalf of the sender - it already sees message content, so it learns nothing new, and unlike the sender its IP is not linked to any identity. + + +## Conclusion + +SimpleX Channels enable a publisher to reach an unlimited audience without any infrastructure operator knowing who that audience is. No third party can seize the channel because owners hold the keys and the authoritative state on their own devices - relays only cache and forward. Owner signatures protect content integrity and the trust chain extends to all administrative roles. These properties require a network without participant identifiers - they cannot be added to a system that has them. diff --git a/docs/protocol/channels-protocol.md b/docs/protocol/channels-protocol.md new file mode 100644 index 0000000000..979ab2c85b --- /dev/null +++ b/docs/protocol/channels-protocol.md @@ -0,0 +1,243 @@ +Revision 1, 2026-04-28 + +# SimpleX Channels Protocol + +For architecture, design rationale, security properties, and threat model, see [SimpleX Channels Overview](./channels-overview.md). + +## Table of contents + +- [Protocol](#protocol) + - [Channel creation](#channel-creation) + - [Relay acceptance](#relay-acceptance) + - [Subscriber connection](#subscriber-connection) + - [Message signing](#message-signing) + - [Message forwarding](#message-forwarding) + - [Binary batch format](#binary-batch-format) + - [Delivery pipeline](#delivery-pipeline) + - [Message deduplication](#message-deduplication) + - [Channel-as-sender messages](#channel-as-sender-messages) + - [Member support scope](#member-support-scope) + + +## Protocol + +This document describes the channel protocol as currently implemented. It builds on the [SimpleX Chat Protocol](./simplex-chat.md), using the same message format and connection model, with extensions for relay-mediated distribution and cryptographic message signing. + +### Channel creation + +Creating a channel involves generating cryptographic material, creating the channel link, and connecting relay members: + +1. **Key generation.** The owner generates an Ed25519 root key pair. The entity ID is computed as `sha256(rootPubKey)`. A separate member key pair is generated for message signing, and an `OwnerAuth` entry is created, signed by the root key. + +2. **Link creation.** The owner calls the agent's `prepareConnectionLink` API with the root key pair and entity ID. This returns a prepared link (including a `ConnShortLink` address) without any network calls. The link address is deterministic, derived from the fixed data hash, so it can be embedded in the group profile immediately. + +3. **Link data upload.** The owner calls `createConnectionForLink`, which makes a single network call to create the SMP queue and upload the encrypted link data. The link's fixed data contains the root public key and connection request. The mutable user data contains the `OwnerAuth` array, the channel profile (including the entity ID and the link itself), and the initial subscriber count. + +4. **Relay invitation.** For each selected relay, the owner sends a contact request containing an `x.grp.relay.inv` message with the channel's short link. The relay retrieves the link data, validates the channel profile, creates its own relay link (with the channel's entity ID in its immutable data), and responds with `x.grp.relay.acpt` containing its relay link. + +5. **Link update.** As each relay accepts and provides its relay link, the owner validates that the relay link contains the correct entity ID, then adds the relay link to the channel link's mutable data. + +6. **Local record.** The channel is stored on the owner's device with the root private key, member private key, and channel profile. This local record is the authoritative state of the channel. + +### Relay acceptance + +When a relay receives an invitation to serve a channel, it validates the channel and creates its own relay link. This flow is currently part of channel creation; adding relays to an existing channel is planned but not yet implemented. + +1. Owner sends `x.grp.relay.inv` to the relay's contact address. This message includes the relay's member ID and role, the owner's profile, and the channel's short link. + +2. Relay receives the invitation and creates a relay request record. A relay request worker processes it asynchronously. + +3. The worker retrieves the channel's link data from the SMP server, extracts and validates the channel profile and owner authorization. + +4. The relay creates its own contact address link (the relay link) with the channel's entity ID in the immutable fixed data. + +5. The relay accepts the owner's connection request, sending its relay link in the acceptance. + +6. The owner retrieves the relay link data, validates that the entity ID in the relay link matches the channel's entity ID, and adds the relay link to the channel link's user data. + +TODO: Periodic monitoring where the relay retrieves channel link data to verify its relay link is still listed is planned but not yet implemented. + +### Subscriber connection + +A subscriber joins a channel through the following flow: + +1. **Link retrieval.** The subscriber scans or receives the channel's short link. The client retrieves the link data, which contains the channel profile, owner authorization chain, and list of relay links. + +2. **Relay link resolution.** For each relay link listed, the client resolves the `ConnectionRequestUri` from the relay's short link. + +3. **Connection.** The client connects to relays - the first synchronously, the rest asynchronously. Each connection sends an `x.member` message with the subscriber's profile (or an incognito profile, created once and shared with all relays), member ID, and member signing key. + +4. **Relay acceptance.** Each relay accepts the connection, creates a member record for the subscriber with the configured subscriber role (default `observer`), and sends an `x.grp.link.inv` message with the channel profile and group link invitation data. + +5. **Introduction.** The relay introduces the new subscriber to the channel's moderators and owners by sending an `x.grp.mem.new` message. It also sends moderator/owner profiles to the subscriber. + +6. **History.** If the channel has history sharing enabled, the relay sends recent cached history to the new subscriber. + +The subscriber is functional (can receive messages) as soon as at least one relay connection succeeds. Additional relay connections provide redundancy and cross-relay consistency checking. + +### Message signing + +Messages that alter the channel's roster, profile, or administrative state are cryptographically signed by the sending owner. Content messages are not signed by default; see [Signing scope](#signing-scope-roster-only-content-optional) for the rationale. + +**Which messages require signatures:** + +| Message | Description | Signed | +|---|---|---| +| `x.grp.del` | Delete channel | Required | +| `x.grp.info` | Update channel profile | Required | +| `x.grp.prefs` | Update channel preferences | Required | +| `x.grp.mem.del` | Remove member | Required | +| `x.grp.mem.role` | Change member role | Required | +| `x.grp.mem.restrict` | Restrict member | Required | +| `x.grp.leave` | Leave channel | Required (unverified allowed between subscribers) | +| `x.info` | Update member profile | Required (unverified allowed between subscribers) | +| `x.msg.new` | Content message | Not signed | +| `x.msg.update` | Edit message | Not signed | +| `x.msg.del` | Delete message | Not signed | + +**Signing process:** + +The signing context binds the signature to a specific channel and sender: + +``` +bindingPrefix = smpEncode(CBGroup) <> smpEncode(publicGroupId, memberId) +signedBytes = bindingPrefix <> messageBody +signature = Ed25519.sign(memberPrivKey, signedBytes) +``` + +The binding prefix includes the chat binding tag (`"G"` for group), the channel's entity ID, and the sender's member ID. This prevents cross-channel and cross-member replay attacks - a signature valid in one channel cannot be reused in another. + +**Verification process:** + +When a subscriber receives a signed message: + +1. The signature is present: reconstruct the binding prefix from the channel's stored entity ID and the sender's member ID. Verify all signatures against the sender's stored public key. If all verify, the message is accepted as verified. + +2. The signature is present but the sender's key is unknown: the message is accepted as signed-but-unverified only if the event does not require a signature. For `x.grp.leave` and `x.info` between subscribers whose keys haven't been exchanged yet, unverified signatures are permitted as a temporary measure. + +3. No signature is present: the message is accepted only if the event does not require a signature (i.e., the channel does not use relays, or the event is a content message). + +If verification fails for a message that requires a signature, the message is rejected and a bad signature event is shown to the user. + +### Message forwarding + +Content originates on the owner's device and flows through relays to subscribers. The forwarding mechanism preserves the original message bytes, including any signature, without re-encoding: + +**Owner to Relay:** The owner sends messages directly to each relay over their SMP connection. Messages are encoded in binary batch format. + +**Relay processing:** When a relay receives a message from an owner, it: + +1. Parses and processes the message locally (updating its cached state, e.g. for roster changes). +2. If the relay is configured to forward for this channel, creates a **delivery task** for each message that should be forwarded to subscribers. The task records the message ID, the sender's member ID, the broker timestamp, and whether the message was sent as the channel (not attributed to a specific owner). +3. The delivery task is persisted to the database for delivery reliability - ensuring forwarding can resume after a relay crash. + +**Relay to Subscribers:** A delivery task worker reads pending tasks, batches them into delivery jobs, and a delivery job worker sends each job to subscribers in paginated batches (using a cursor over group member IDs). + +For forwarded messages from subscribers to owners (e.g. support scope messages), the relay wraps the message in a forwarding envelope: + +``` +forwardEnvelope = ">" <> smpEncode(GrpMsgForward) <> encodeBatchElement(signedMsg, msgBody) +``` + +This preserves the original message's signature bytes verbatim. + +### Binary batch format + +Channels use a binary batch format that preserves exact message bytes for signature verification. This is distinct from the JSON array batching used by regular groups. + +```abnf +binaryBatch = %s"=" elementCount *batchElement +elementCount = 1*1 OCTET ; 1-255 elements +batchElement = elementLen elementBody +elementLen = 2*2 OCTET ; 16-bit big-endian length + +elementBody = signedElement / forwardElement / plainElement / fileElement + +signedElement = %s"/" chatBinding sigCount *msgSignature jsonBody +forwardElement = %s">" grpMsgForward (signedElement / plainElement) +plainElement = %s"{" *OCTET ; JSON message body +fileElement = %s"F" *OCTET ; binary file chunk + +chatBinding = 1*1 OCTET ; "G" (group), "D" (direct), "C" (channel) +sigCount = 1*1 OCTET ; number of signatures (1-255) +msgSignature = keyRef sigBytes +keyRef = %s"M" ; member key reference +sigBytes = 64*64 OCTET ; Ed25519 signature + +grpMsgForward = fwdSender brokerTs +fwdSender = memberFwd / channelFwd +memberFwd = %s"M" memberId memberName ; attributed to specific member +channelFwd = %s"C" ; attributed to channel as sender +brokerTs = 8*8 OCTET ; UTC system time +``` + +The parser (`parseChatMessages`) dispatches on the first byte: + +- `'='` -> binary batch (new format, used by channels) +- `'X'` -> compressed (decompress, then re-parse) +- `'['` -> JSON array (legacy group format) +- `'{'` -> single JSON message + +Forward elements contain the original message bytes verbatim. The relay does not re-encode the inner message. This is what makes signature verification possible after forwarding: the exact bytes that were signed by the owner are preserved through the relay. + +Nested forwarding (`>` inside `>`) is explicitly rejected by the parser. + +### Delivery pipeline + +The relay's delivery pipeline has two stages, both backed by persistent database tables for delivery reliability (not for authoritative storage - the relay's database is a delivery queue, not a content database): + +**Stage 1: Delivery tasks.** When the relay receives a message from an owner that should be forwarded, it creates a `delivery_task` record: + +``` +delivery_task: + group_id, worker_scope, job_scope, + sender_group_member_id, message_id, + message_from_channel (bool), + task_status (new -> processed) +``` + +A **task worker** (one per group per scope) reads pending tasks, batches multiple tasks into a single binary batch body, and creates a delivery job. + +**Stage 2: Delivery jobs.** A delivery job contains the pre-encoded batch body and a cursor for paginated delivery: + +``` +delivery_job: + group_id, worker_scope, job_scope, + body (pre-encoded binary batch), + cursor_group_member_id, + job_status (pending -> complete) +``` + +A **job worker** reads the body and delivers it to subscribers in paginated batches. For each page, it loads a bucket of subscribers by cursor position, sends the body to all of them, advances the cursor, and continues until all subscribers have been served. This avoids loading all subscribers into memory at once. + +For subsequent subscribers in a batch, the agent uses a value reference to the first subscriber's message body, avoiding redundant data transmission to the SMP server. + +### Message deduplication + +When multiple relays serve the same channel, each subscriber receives the same message from each relay independently. Deduplication is performed at the subscriber's client level using the message's shared message ID: + +- When saving a received message, the client checks whether a message with the same shared ID already exists for this group. +- If a duplicate is found, the message is silently dropped (in channels with relays). +- In non-relay groups, duplicate detection triggers a `x.grp.mem.con` notification to the forwarding member. + +This is essentially cache coherence verification - comparing what was received from one cache node against another. TODO: Currently, deduplication only detects the presence of duplicates. The protocol design includes provisions for detecting differences between relay-delivered copies of the same message (hash comparison, UI indicators for discrepancies). This is described in the [channels forwarding RFC](../rfcs/2025-08-11-channels-forwarding.md) and is not yet implemented. + +### Channel-as-sender messages + +Owners can send messages attributed to the channel rather than to themselves. When `asGroup = True` is set in the message container, the relay forwards the message with a channel-as-sender tag instead of attributing it to a specific member. On the subscriber side, such messages are displayed as coming from the channel (using the channel's profile image and name) rather than from a specific owner. + +This will be useful for channels with multiple owners (not yet implemented at application level) where the identity of the specific sender should not be visible to subscribers. The relay must respect this directive; ignoring it and revealing the sending owner's identity is a threat vector (detectable out-of-band by members communicating with the owner). + +The forwarding binding prefix for channel-as-sender messages uses `CBChannel` instead of `CBGroup`, and includes only the channel's entity ID (not the sender's member ID): + +``` +channelBinding = smpEncode(CBChannel) <> smpEncode(publicGroupId) +``` + +### Member support scope + +Channels support a **member support scope** - a private side-channel between a subscriber and the channel's moderators/owners. Messages sent in the support scope are delivered only to moderators and the scoped subscriber, not to all subscribers. + +A support-scoped message includes the target member's ID. The delivery pipeline uses a separate job scope for support messages, loading only the scoped member and moderators rather than all subscribers. + +Support scope messages are visible only to the subscriber who initiated the support conversation and to the channel's moderators. Other subscribers cannot see them. This allows subscribers to report issues, appeal moderation decisions, or communicate with administrators without revealing their identity to other subscribers. diff --git a/docs/protocol/simplex-chat.md b/docs/protocol/simplex-chat.md index c74465f6fe..040064864c 100644 --- a/docs/protocol/simplex-chat.md +++ b/docs/protocol/simplex-chat.md @@ -266,6 +266,12 @@ Currently members can have one of four roles - `owner`, `admin`, `member` and `o `x.grp.msg.forward` message is sent by inviting member to forward messages between introduced members, while they are connecting. +### Channels: relay-mediated groups + +Channels are groups where message delivery is mediated by dedicated relay members rather than by direct connections between all members. Channels extend the group sub-protocol with additional roles (`relay`, `observer`), message signing for administrative actions, a binary batch format for signed and forwarded messages, and an asynchronous delivery pipeline. + +For architecture and design rationale, see [SimpleX Channels Overview](./channels-overview.md). For protocol-level detail - wire formats, message types, signing mechanics, delivery pipeline - see [SimpleX Channels Protocol](./channels-protocol.md). + ## Sub-protocol for WebRTC audio/video calls This sub-protocol is used to send call invitations and to negotiate end-to-end encryption keys and pass WebRTC signalling information. @@ -282,12 +288,13 @@ These message are used for WebRTC calls: ## Threat model -This threat model compliments SMP, XFTP, push notifications and XRCP protocols threat models: +This threat model complements SMP, XFTP, push notifications and XRCP protocols threat models, as well as the channel-specific threat model: - [SimpleX Messaging Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md#threat-model); - [SimpleX File Transfer Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/xftp.md#threat-model); - [Push notifications threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/push-notifications.md#threat-model); -- [SimpleX Remote Control Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/xrcp.md#threat-model). +- [SimpleX Remote Control Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/xrcp.md#threat-model); +- [SimpleX Channels threat model](./channels-overview.md#threat-model). #### A user's contact @@ -342,3 +349,27 @@ This threat model compliments SMP, XFTP, push notifications and XRCP protocols t *cannot:* - prove that two group members with incognito profiles is the same user. + +#### A channel relay + +For the full channel threat model, see [SimpleX Channels: threat model](./channels-overview.md#threat-model). + +*can:* + +- send arbitrary unsigned content messages to subscribers, effectively fabricating the content stream while the channel identity and signed profile remain intact. + +- selectively drop any messages, both content and signed administrative events, for some or all subscribers. + +- ignore the "message from channel" directive, revealing which specific owner sent a message. + +- fabricate subscriber connections, inflating subscriber counts. + +*cannot:* + +- impersonate an owner - administrative messages (roster changes, profile updates, channel deletion) require valid cryptographic signatures that the relay cannot produce. + +- substitute the channel profile - profile changes require a valid owner signature. + +- redirect joining subscribers to a different channel - the channel's entity ID is baked into both the channel link and the relay link's immutable data. + +- determine the real-world identity of subscribers - subscriber connections carry no persistent identity. diff --git a/flake.nix b/flake.nix index c130e1a1fd..43f4e8912a 100644 --- a/flake.nix +++ b/flake.nix @@ -93,6 +93,7 @@ for pkg in $out/_pkg/*.a; do chmod +w $pkg ${mac2ios.packages.${system}.mac2ios}/bin/mac2ios $pkg + [[ "$pkg" == *simplex-chat* ]] && ${pkgs.stdenv.cc.targetPrefix}strip -x $pkg chmod -w $pkg done diff --git a/packages/simplex-chat-client/types/typescript/package.json b/packages/simplex-chat-client/types/typescript/package.json index a135b286c2..1d5eb5197c 100644 --- a/packages/simplex-chat-client/types/typescript/package.json +++ b/packages/simplex-chat-client/types/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@simplex-chat/types", - "version": "0.3.0", + "version": "0.6.0", "description": "TypeScript types for SimpleX Chat bot libraries", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index d5c3046e3a..f8aa6e445d 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -351,7 +351,7 @@ export interface APINewPublicGroup { } export namespace APINewPublicGroup { - export type Response = CR.PublicGroupCreated | CR.ChatCmdError + export type Response = CR.PublicGroupCreated | CR.PublicGroupCreationFailed | CR.ChatCmdError export function cmdString(self: APINewPublicGroup): string { return '/_public group ' + self.userId + (self.incognito ? ' incognito=on' : '') + ' ' + self.relayIds.join(',') + ' ' + JSON.stringify(self.groupProfile) @@ -372,6 +372,21 @@ export namespace APIGetGroupRelays { } } +// Add relays to group. +// Network usage: interactive. +export interface APIAddGroupRelays { + groupId: number // int64 + relayIds: number[] // int64, non-empty +} + +export namespace APIAddGroupRelays { + export type Response = CR.GroupRelaysAdded | CR.GroupRelaysAddFailed | CR.ChatCmdError + + export function cmdString(self: APIAddGroupRelays): string { + return '/_add relays #' + self.groupId + ' ' + self.relayIds.join(',') + } +} + // Update group profile. // Network usage: background. export interface APIUpdateGroupProfile { @@ -471,6 +486,8 @@ export namespace APIAddContact { export interface APIConnectPlan { userId: number // int64 connectionLink?: string + resolveKnown: boolean + linkOwnerSig?: T.LinkOwnerSig } export namespace APIConnectPlan { @@ -573,6 +590,23 @@ export namespace APIListGroups { } } +// Get chat previews. Supports time-based pagination — use this instead of APIListContacts / APIListGroups when scanning at scale (those load every record into memory and fail on large databases). +// Network usage: no. +export interface APIGetChats { + userId: number // int64 + pendingConnections: boolean + pagination: T.PaginationByTime + query: T.ChatListQuery +} + +export namespace APIGetChats { + export type Response = CR.ApiChats | CR.ChatCmdError + + export function cmdString(self: APIGetChats): string { + return '/_get chats ' + self.userId + (self.pendingConnections ? ' pcc=on' : '') + ' ' + T.PaginationByTime.cmdString(self.pagination) + ' ' + JSON.stringify(self.query) + } +} + // Delete chat. // Network usage: background. export interface APIDeleteChat { diff --git a/packages/simplex-chat-client/types/typescript/src/responses.ts b/packages/simplex-chat-client/types/typescript/src/responses.ts index 8d4f68c000..e4284bf87e 100644 --- a/packages/simplex-chat-client/types/typescript/src/responses.ts +++ b/packages/simplex-chat-client/types/typescript/src/responses.ts @@ -28,7 +28,10 @@ export type ChatResponse = | CR.GroupLinkDeleted | CR.GroupCreated | CR.PublicGroupCreated + | CR.PublicGroupCreationFailed | CR.GroupRelays + | CR.GroupRelaysAdded + | CR.GroupRelaysAddFailed | CR.GroupMembers | CR.GroupUpdated | CR.GroupsList @@ -54,6 +57,7 @@ export type ChatResponse = | CR.UserProfileUpdated | CR.UserProfileNoChange | CR.UsersList + | CR.ApiChats export namespace CR { export type Tag = @@ -81,7 +85,10 @@ export namespace CR { | "groupLinkDeleted" | "groupCreated" | "publicGroupCreated" + | "publicGroupCreationFailed" | "groupRelays" + | "groupRelaysAdded" + | "groupRelaysAddFailed" | "groupMembers" | "groupUpdated" | "groupsList" @@ -107,6 +114,7 @@ export namespace CR { | "userProfileUpdated" | "userProfileNoChange" | "usersList" + | "apiChats" interface Interface { type: Tag @@ -258,6 +266,12 @@ export namespace CR { groupRelays: T.GroupRelay[] } + export interface PublicGroupCreationFailed extends Interface { + type: "publicGroupCreationFailed" + user: T.User + addRelayResults: T.AddRelayResult[] + } + export interface GroupRelays extends Interface { type: "groupRelays" user: T.User @@ -265,6 +279,20 @@ export namespace CR { groupRelays: T.GroupRelay[] } + export interface GroupRelaysAdded extends Interface { + type: "groupRelaysAdded" + user: T.User + groupInfo: T.GroupInfo + groupLink: T.GroupLink + groupRelays: T.GroupRelay[] + } + + export interface GroupRelaysAddFailed extends Interface { + type: "groupRelaysAddFailed" + user: T.User + addRelayResults: T.AddRelayResult[] + } + export interface GroupMembers extends Interface { type: "groupMembers" user: T.User @@ -435,4 +463,10 @@ export namespace CR { type: "usersList" users: T.UserInfo[] } + + export interface ApiChats extends Interface { + type: "apiChats" + user: T.User + chats: T.AChat[] + } } diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 274ddb6924..46ba695e61 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -17,6 +17,11 @@ export interface AChatItem { chatItem: ChatItem } +export interface AddRelayResult { + relay: UserChatRelay + relayError?: ChatError +} + export interface AddressSettings { businessAddress: boolean autoAccept?: AutoAccept @@ -278,6 +283,7 @@ export type CIContent = | CIContent.RcvCall | CIContent.RcvIntegrityError | CIContent.RcvDecryptionError + | CIContent.RcvMsgError | CIContent.RcvGroupInvitation | CIContent.SndGroupInvitation | CIContent.RcvDirectEvent @@ -312,6 +318,7 @@ export namespace CIContent { | "rcvCall" | "rcvIntegrityError" | "rcvDecryptionError" + | "rcvMsgError" | "rcvGroupInvitation" | "sndGroupInvitation" | "rcvDirectEvent" @@ -383,6 +390,11 @@ export namespace CIContent { msgCount: number // word32 } + export interface RcvMsgError extends Interface { + type: "rcvMsgError" + rcvMsgError: RcvMsgError + } + export interface RcvGroupInvitation extends Interface { type: "rcvGroupInvitation" groupInvitation: CIGroupInvitation @@ -1559,6 +1571,27 @@ export interface ChatItemDeletion { toChatItem?: AChatItem } +export type ChatListQuery = ChatListQuery.Filters | ChatListQuery.Search + +export namespace ChatListQuery { + export type Tag = "filters" | "search" + + interface Interface { + type: Tag + } + + export interface Filters extends Interface { + type: "filters" + favorite: boolean + unread: boolean + } + + export interface Search extends Interface { + type: "search" + search: string + } +} + export enum ChatPeerType { Human = "human", Bot = "bot", @@ -1708,6 +1741,11 @@ export namespace CommandErrorType { } } +export interface CommentsGroupPreference { + enable: GroupFeatureEnabled + duration?: number // int +} + export interface ComposedMessage { fileSource?: CryptoFile quotedItemId?: number // int64 @@ -1968,6 +2006,7 @@ export namespace ContactAddressPlan { export interface Ok extends Interface { type: "ok" contactSLinkData_?: ContactShortLinkData + ownerVerification?: OwnerVerification } export interface OwnLink extends Interface { @@ -2064,7 +2103,13 @@ export interface CryptoFileArgs { fileNonce: string } +export interface DroppedMsg { + brokerTs: string // ISO-8601 timestamp + attempts: number // int +} + export interface E2EInfo { + public?: boolean pqEnabled?: boolean } @@ -2413,7 +2458,9 @@ export interface FullGroupPreferences { simplexLinks: RoleGroupPreference reports: GroupPreference history: GroupPreference + support: SupportGroupPreference sessions: RoleGroupPreference + comments: CommentsGroupPreference commands: ChatBotCommand[] } @@ -2485,7 +2532,9 @@ export enum GroupFeature { SimplexLinks = "simplexLinks", Reports = "reports", History = "history", + Support = "support", Sessions = "sessions", + Comments = "comments", } export enum GroupFeatureEnabled { @@ -2534,15 +2583,27 @@ export interface GroupLink { acceptMemberRole: GroupMemberRole } +export interface GroupLinkOwner { + memberId: string + memberKey: string +} + export type GroupLinkPlan = | GroupLinkPlan.Ok | GroupLinkPlan.OwnLink | GroupLinkPlan.ConnectingConfirmReconnect | GroupLinkPlan.ConnectingProhibit | GroupLinkPlan.Known + | GroupLinkPlan.NoRelays export namespace GroupLinkPlan { - export type Tag = "ok" | "ownLink" | "connectingConfirmReconnect" | "connectingProhibit" | "known" + export type Tag = + | "ok" + | "ownLink" + | "connectingConfirmReconnect" + | "connectingProhibit" + | "known" + | "noRelays" interface Interface { type: Tag @@ -2552,6 +2613,7 @@ export namespace GroupLinkPlan { type: "ok" groupSLinkInfo_?: GroupShortLinkInfo groupSLinkData_?: GroupShortLinkData + ownerVerification?: OwnerVerification } export interface OwnLink extends Interface { @@ -2571,6 +2633,14 @@ export namespace GroupLinkPlan { export interface Known extends Interface { type: "known" groupInfo: GroupInfo + groupUpdated: boolean + ownerVerification?: OwnerVerification + linkOwners: GroupLinkOwner[] + } + + export interface NoRelays extends Interface { + type: "noRelays" + groupSLinkData_?: GroupShortLinkData } } @@ -2662,7 +2732,9 @@ export interface GroupPreferences { simplexLinks?: RoleGroupPreference reports?: GroupPreference history?: GroupPreference + support?: SupportGroupPreference sessions?: RoleGroupPreference + comments?: CommentsGroupPreference commands?: ChatBotCommand[] } @@ -2731,6 +2803,7 @@ export interface GroupSupportChat { export enum GroupType { Channel = "channel", + Group = "group", } export enum HandshakeError { @@ -2761,6 +2834,7 @@ export namespace InvitationLinkPlan { export interface Ok extends Interface { type: "ok" contactSLinkData_?: ContactShortLinkData + ownerVerification?: OwnerVerification } export interface OwnLink extends Interface { @@ -2830,6 +2904,12 @@ export namespace LinkContent { } } +export interface LinkOwnerSig { + ownerId?: string + chatBinding: string + ownerSig: string +} + export interface LinkPreview { uri: string title: string @@ -2947,6 +3027,7 @@ export namespace MsgContent { type: "chat" text: string chatLink: MsgChatLink + ownerSig?: LinkOwnerSig } export interface Unknown extends Interface { @@ -3106,6 +3187,44 @@ export interface NoteFolder { unread: boolean } +export type OwnerVerification = OwnerVerification.Verified | OwnerVerification.Failed + +export namespace OwnerVerification { + export type Tag = "verified" | "failed" + + interface Interface { + type: Tag + } + + export interface Verified extends Interface { + type: "verified" + } + + export interface Failed extends Interface { + type: "failed" + reason: string + } +} + +export type PaginationByTime = PaginationByTime.Last + +export namespace PaginationByTime { + export type Tag = "last" + + interface Interface { + type: Tag + } + + export interface Last extends Interface { + type: "last" + count: number // int + } + + export function cmdString(self: PaginationByTime): string { + return 'count=' + self.count + } +} + export interface PendingContactConnection { pccConnId: number // int64 pccAgentConnId: string @@ -3593,6 +3712,26 @@ export namespace RcvGroupEvent { } } +export type RcvMsgError = RcvMsgError.Dropped | RcvMsgError.ParseError + +export namespace RcvMsgError { + export type Tag = "dropped" | "parseError" + + interface Interface { + type: Tag + } + + export interface Dropped extends Interface { + type: "dropped" + attempts: number // int + } + + export interface ParseError extends Interface { + type: "parseError" + parseError: string + } +} + export interface RelayProfile { displayName: string fullName: string @@ -3605,6 +3744,7 @@ export enum RelayStatus { Invited = "invited", Accepted = "accepted", Active = "active", + Inactive = "inactive", } export enum ReportReason { @@ -3668,6 +3808,7 @@ export namespace SMPAgentError { export interface A_DUPLICATE extends Interface { type: "A_DUPLICATE" + droppedMsg_?: DroppedMsg } export interface A_QUEUE extends Interface { @@ -4534,6 +4675,10 @@ export namespace SubscriptionStatus { } } +export interface SupportGroupPreference { + enable: GroupFeatureEnabled +} + export enum SwitchPhase { Started = "started", Confirmed = "confirmed", diff --git a/packages/simplex-chat-nodejs/README.md b/packages/simplex-chat-nodejs/README.md index e75dab3f96..739b41b34e 100644 --- a/packages/simplex-chat-nodejs/README.md +++ b/packages/simplex-chat-nodejs/README.md @@ -14,7 +14,7 @@ Please share your use cases and implementations. ## Quick start: a simple bot ``` -npm i simplex-chat@6.5.0-beta.4.4 +npm i simplex-chat@6.5.1 ``` Simple bot that replies with squares of numbers you send to it: @@ -26,7 +26,7 @@ Simple bot that replies with squares of numbers you send to it: // const {bot} = await import("../dist/index.js") const [chat, _user, _address] = await bot.run({ profile: {displayName: "Squaring bot example", fullName: ""}, - dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""}, + dbOpts: {type: "sqlite", filePrefix: "./squaring_bot"}, options: { addressSettings: {welcomeMessage: "Send a number, I will square it.", }, @@ -62,6 +62,44 @@ There is an example with more options in [./examples/squaring-bot.ts](./examples You can run it with: `npx ts-node ./examples/squaring-bot.ts` +## PostgreSQL backend + +By default, the package uses SQLite. To use PostgreSQL instead: + +```bash +npm install simplex-chat --simplex_backend=postgres +``` + +Or persist the setting in `.npmrc`: + +```ini +simplex_backend=postgres +``` + +### Prerequisites (PostgreSQL) + +- `libpq5` must be installed on the host system (`apt install libpq5` on Debian/Ubuntu) +- PostgreSQL backend is only available for Linux x86_64 +- A PostgreSQL server accessible via connection string + +### Passing PostgreSQL connection + +The `DbConfig` type is a discriminated union — pick the variant that matches +the backend you installed: + +```ts +// SQLite (default) +dbOpts: {type: "sqlite", filePrefix: "./data/bot"} +// optional: encryptionKey: "" + +// PostgreSQL +dbOpts: { + type: "postgres", + connectionString: "postgres://user:pass@host/db", + // schemaPrefix: "bot", // optional — defaults to "simplex_v1" +} +``` + ## Documentation The library docs are [here](./docs/README.md). diff --git a/packages/simplex-chat-nodejs/docs/Namespace.api.md b/packages/simplex-chat-nodejs/docs/Namespace.api.md index a1b3d2ca5a..5771b8450e 100644 --- a/packages/simplex-chat-nodejs/docs/Namespace.api.md +++ b/packages/simplex-chat-nodejs/docs/Namespace.api.md @@ -24,6 +24,7 @@ You need to use it in bot event handlers, and for any other use cases. ## Type Aliases +- [DbConfig](api.TypeAlias.DbConfig.md) - [EventSubscriberFunc](api.TypeAlias.EventSubscriberFunc.md) - [EventSubscribers](api.TypeAlias.EventSubscribers.md) diff --git a/packages/simplex-chat-nodejs/docs/Namespace.bot.md b/packages/simplex-chat-nodejs/docs/Namespace.bot.md index 447b0b6f68..4b9171d0f7 100644 --- a/packages/simplex-chat-nodejs/docs/Namespace.bot.md +++ b/packages/simplex-chat-nodejs/docs/Namespace.bot.md @@ -12,9 +12,12 @@ It automates creating and updating of the bot profile, address and bot commands ## Interfaces - [BotConfig](bot.Interface.BotConfig.md) -- [BotDbOpts](bot.Interface.BotDbOpts.md) - [BotOptions](bot.Interface.BotOptions.md) +## Type Aliases + +- [BotDbOpts](bot.TypeAlias.BotDbOpts.md) + ## Functions - [run](bot.Function.run.md) diff --git a/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md b/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md index 6812740aa2..b81677b976 100644 --- a/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md +++ b/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md @@ -6,7 +6,7 @@ # Class: ChatApi -Defined in: [src/api.ts:62](../src/api.ts#L62) +Defined in: [src/api.ts:97](../src/api.ts#L97) Main API class for interacting with the chat core library. @@ -16,7 +16,7 @@ Main API class for interacting with the chat core library. > `protected` **ctrl\_**: `bigint` \| `undefined` -Defined in: [src/api.ts:68](../src/api.ts#L68) +Defined in: [src/api.ts:103](../src/api.ts#L103) ## Accessors @@ -26,7 +26,7 @@ Defined in: [src/api.ts:68](../src/api.ts#L68) > **get** **ctrl**(): `bigint` -Defined in: [src/api.ts:295](../src/api.ts#L295) +Defined in: [src/api.ts:329](../src/api.ts#L329) Chat controller reference @@ -42,7 +42,7 @@ Chat controller reference > **get** **initialized**(): `boolean` -Defined in: [src/api.ts:281](../src/api.ts#L281) +Defined in: [src/api.ts:315](../src/api.ts#L315) Chat controller is initialized @@ -58,7 +58,7 @@ Chat controller is initialized > **get** **started**(): `boolean` -Defined in: [src/api.ts:288](../src/api.ts#L288) +Defined in: [src/api.ts:322](../src/api.ts#L322) Chat controller is started @@ -72,7 +72,7 @@ Chat controller is started > **apiAcceptContactRequest**(`contactReqId`): `Promise`\<`Contact`\> -Defined in: [src/api.ts:697](../src/api.ts#L697) +Defined in: [src/api.ts:731](../src/api.ts#L731) Accept contact request. Network usage: interactive. @@ -93,7 +93,7 @@ Network usage: interactive. > **apiAcceptMember**(`groupId`, `groupMemberId`, `memberRole`): `Promise`\<`GroupMember`\> -Defined in: [src/api.ts:517](../src/api.ts#L517) +Defined in: [src/api.ts:551](../src/api.ts#L551) Accept group member. Requires Admin role. Network usage: background. @@ -122,7 +122,7 @@ Network usage: background. > **apiAddMember**(`groupId`, `contactId`, `memberRole`): `Promise`\<`GroupMember`\> -Defined in: [src/api.ts:497](../src/api.ts#L497) +Defined in: [src/api.ts:531](../src/api.ts#L531) Add contact to group. Requires bot to have Admin role. Network usage: interactive. @@ -151,7 +151,7 @@ Network usage: interactive. > **apiBlockMembersForAll**(`groupId`, `groupMemberIds`, `blocked`): `Promise`\<`void`\> -Defined in: [src/api.ts:537](../src/api.ts#L537) +Defined in: [src/api.ts:571](../src/api.ts#L571) Block members. Requires Moderator role. Network usage: background. @@ -180,7 +180,7 @@ Network usage: background. > **apiCancelFile**(`fileId`): `Promise`\<`void`\> -Defined in: [src/api.ts:487](../src/api.ts#L487) +Defined in: [src/api.ts:521](../src/api.ts#L521) Cancel file. Network usage: background. @@ -201,7 +201,7 @@ Network usage: background. > **apiChatItemReaction**(`chatType`, `chatId`, `chatItemId`, `add`, `reaction`): `Promise`\<`ChatItemDeletion`[]\> -Defined in: [src/api.ts:461](../src/api.ts#L461) +Defined in: [src/api.ts:495](../src/api.ts#L495) Add/remove message reaction. Network usage: background. @@ -238,7 +238,7 @@ Network usage: background. > **apiConnect**(`userId`, `incognito`, `preparedLink?`): `Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\> -Defined in: [src/api.ts:666](../src/api.ts#L666) +Defined in: [src/api.ts:700](../src/api.ts#L700) Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link Network usage: interactive. @@ -267,7 +267,7 @@ Network usage: interactive. > **apiConnectActiveUser**(`connLink`): `Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\> -Defined in: [src/api.ts:675](../src/api.ts#L675) +Defined in: [src/api.ts:709](../src/api.ts#L709) Connect via SimpleX link as string in the active user profile. Network usage: interactive. @@ -288,7 +288,7 @@ Network usage: interactive. > **apiConnectPlan**(`userId`, `connectionLink`): `Promise`\<\[`ConnectionPlan`, `CreatedConnLink`\]\> -Defined in: [src/api.ts:656](../src/api.ts#L656) +Defined in: [src/api.ts:690](../src/api.ts#L690) Determine SimpleX link type and if the bot is already connected via this link. Network usage: interactive. @@ -313,7 +313,7 @@ Network usage: interactive. > **apiCreateActiveUser**(`profile?`): `Promise`\<`User`\> -Defined in: [src/api.ts:774](../src/api.ts#L774) +Defined in: [src/api.ts:849](../src/api.ts#L849) Create new user profile Network usage: no. @@ -334,7 +334,7 @@ Network usage: no. > **apiCreateGroupLink**(`groupId`, `memberRole`): `Promise`\<`string`\> -Defined in: [src/api.ts:597](../src/api.ts#L597) +Defined in: [src/api.ts:631](../src/api.ts#L631) Create group link. Network usage: interactive. @@ -359,7 +359,7 @@ Network usage: interactive. > **apiCreateLink**(`userId`): `Promise`\<`string`\> -Defined in: [src/api.ts:643](../src/api.ts#L643) +Defined in: [src/api.ts:677](../src/api.ts#L677) Create 1-time invitation link. Network usage: interactive. @@ -376,11 +376,37 @@ Network usage: interactive. *** +### apiCreateMemberContact() + +> **apiCreateMemberContact**(`groupId`, `groupMemberId`): `Promise`\<`Contact`\> + +Defined in: [src/api.ts:915](../src/api.ts#L915) + +Create a direct message contact with a group member. +Returns the created contact. +Network usage: interactive. + +#### Parameters + +##### groupId + +`number` + +##### groupMemberId + +`number` + +#### Returns + +`Promise`\<`Contact`\> + +*** + ### apiCreateUserAddress() > **apiCreateUserAddress**(`userId`): `Promise`\<`CreatedConnLink`\> -Defined in: [src/api.ts:312](../src/api.ts#L312) +Defined in: [src/api.ts:346](../src/api.ts#L346) Create bot address. Network usage: interactive. @@ -399,9 +425,9 @@ Network usage: interactive. ### apiDeleteChat() -> **apiDeleteChat**(`chatType`, `chatId`, `deleteMode`): `Promise`\<`void`\> +> **apiDeleteChat**(`chatType`, `chatId`, `deleteMode?`): `Promise`\<`void`\> -Defined in: [src/api.ts:737](../src/api.ts#L737) +Defined in: [src/api.ts:771](../src/api.ts#L771) Delete chat. Network usage: background. @@ -416,7 +442,7 @@ Network usage: background. `number` -##### deleteMode +##### deleteMode? `ChatDeleteMode` = `...` @@ -430,7 +456,7 @@ Network usage: background. > **apiDeleteChatItems**(`chatType`, `chatId`, `chatItemIds`, `deleteMode`): `Promise`\<`ChatItemDeletion`[]\> -Defined in: [src/api.ts:436](../src/api.ts#L436) +Defined in: [src/api.ts:470](../src/api.ts#L470) Delete message. Network usage: background. @@ -463,7 +489,7 @@ Network usage: background. > **apiDeleteGroupLink**(`groupId`): `Promise`\<`void`\> -Defined in: [src/api.ts:619](../src/api.ts#L619) +Defined in: [src/api.ts:653](../src/api.ts#L653) Delete group link. Network usage: background. @@ -484,7 +510,7 @@ Network usage: background. > **apiDeleteMemberChatItem**(`groupId`, `chatItemIds`): `Promise`\<`ChatItemDeletion`[]\> -Defined in: [src/api.ts:451](../src/api.ts#L451) +Defined in: [src/api.ts:485](../src/api.ts#L485) Moderate message. Requires Moderator role (and higher than message author's). Network usage: background. @@ -509,7 +535,7 @@ Network usage: background. > **apiDeleteUser**(`userId`, `delSMPQueues`, `viewPwd?`): `Promise`\<`void`\> -Defined in: [src/api.ts:804](../src/api.ts#L804) +Defined in: [src/api.ts:879](../src/api.ts#L879) Delete user profile. Network usage: background. @@ -538,7 +564,7 @@ Network usage: background. > **apiDeleteUserAddress**(`userId`): `Promise`\<`void`\> -Defined in: [src/api.ts:322](../src/api.ts#L322) +Defined in: [src/api.ts:356](../src/api.ts#L356) Deletes a user address. Network usage: background. @@ -559,7 +585,7 @@ Network usage: background. > **apiGetActiveUser**(): `Promise`\<`User` \| `undefined`\> -Defined in: [src/api.ts:754](../src/api.ts#L754) +Defined in: [src/api.ts:829](../src/api.ts#L829) Get active user profile Network usage: no. @@ -570,11 +596,40 @@ Network usage: no. *** +### apiGetChat() + +> **apiGetChat**(`chatType`, `chatId`, `count`): `Promise`\<`any`\> + +Defined in: [src/api.ts:819](../src/api.ts#L819) + +Get chat items. +Network usage: no. + +#### Parameters + +##### chatType + +`ChatType` + +##### chatId + +`number` + +##### count + +`number` + +#### Returns + +`Promise`\<`any`\> + +*** + ### apiGetGroupLink() > **apiGetGroupLink**(`groupId`): `Promise`\<`GroupLink`\> -Defined in: [src/api.ts:628](../src/api.ts#L628) +Defined in: [src/api.ts:662](../src/api.ts#L662) Get group link. Network usage: no. @@ -595,7 +650,7 @@ Network usage: no. > **apiGetGroupLinkStr**(`groupId`): `Promise`\<`string`\> -Defined in: [src/api.ts:634](../src/api.ts#L634) +Defined in: [src/api.ts:668](../src/api.ts#L668) #### Parameters @@ -613,7 +668,7 @@ Defined in: [src/api.ts:634](../src/api.ts#L634) > **apiGetUserAddress**(`userId`): `Promise`\<`UserContactLink` \| `undefined`\> -Defined in: [src/api.ts:332](../src/api.ts#L332) +Defined in: [src/api.ts:366](../src/api.ts#L366) Get bot address and settings. Network usage: no. @@ -634,7 +689,7 @@ Network usage: no. > **apiJoinGroup**(`groupId`): `Promise`\<`GroupInfo`\> -Defined in: [src/api.ts:507](../src/api.ts#L507) +Defined in: [src/api.ts:541](../src/api.ts#L541) Join group. Network usage: interactive. @@ -655,7 +710,7 @@ Network usage: interactive. > **apiLeaveGroup**(`groupId`): `Promise`\<`GroupInfo`\> -Defined in: [src/api.ts:557](../src/api.ts#L557) +Defined in: [src/api.ts:591](../src/api.ts#L591) Leave group. Network usage: background. @@ -676,7 +731,7 @@ Network usage: background. > **apiListContacts**(`userId`): `Promise`\<`Contact`[]\> -Defined in: [src/api.ts:717](../src/api.ts#L717) +Defined in: [src/api.ts:751](../src/api.ts#L751) Get contacts. Network usage: no. @@ -697,7 +752,7 @@ Network usage: no. > **apiListGroups**(`userId`, `contactId?`, `search?`): `Promise`\<`GroupInfo`[]\> -Defined in: [src/api.ts:727](../src/api.ts#L727) +Defined in: [src/api.ts:761](../src/api.ts#L761) Get groups. Network usage: no. @@ -726,7 +781,7 @@ Network usage: no. > **apiListMembers**(`groupId`): `Promise`\<`GroupMember`[]\> -Defined in: [src/api.ts:567](../src/api.ts#L567) +Defined in: [src/api.ts:601](../src/api.ts#L601) Get group members. Network usage: no. @@ -747,7 +802,7 @@ Network usage: no. > **apiListUsers**(): `Promise`\<`UserInfo`[]\> -Defined in: [src/api.ts:784](../src/api.ts#L784) +Defined in: [src/api.ts:859](../src/api.ts#L859) Get all user profiles Network usage: no. @@ -762,7 +817,7 @@ Network usage: no. > **apiNewGroup**(`userId`, `groupProfile`): `Promise`\<`GroupInfo`\> -Defined in: [src/api.ts:577](../src/api.ts#L577) +Defined in: [src/api.ts:611](../src/api.ts#L611) Create group. Network usage: no. @@ -787,7 +842,7 @@ Network usage: no. > **apiReceiveFile**(`fileId`): `Promise`\<`AChatItem`\> -Defined in: [src/api.ts:477](../src/api.ts#L477) +Defined in: [src/api.ts:511](../src/api.ts#L511) Receive file. Network usage: no. @@ -808,7 +863,7 @@ Network usage: no. > **apiRejectContactRequest**(`contactReqId`): `Promise`\<`void`\> -Defined in: [src/api.ts:707](../src/api.ts#L707) +Defined in: [src/api.ts:741](../src/api.ts#L741) Reject contact request. The user who sent the request is **not notified**. Network usage: no. @@ -827,9 +882,9 @@ Network usage: no. ### apiRemoveMembers() -> **apiRemoveMembers**(`groupId`, `memberIds`, `withMessages`): `Promise`\<`GroupMember`[]\> +> **apiRemoveMembers**(`groupId`, `memberIds`, `withMessages?`): `Promise`\<`GroupMember`[]\> -Defined in: [src/api.ts:547](../src/api.ts#L547) +Defined in: [src/api.ts:581](../src/api.ts#L581) Remove members. Requires Admin role. Network usage: background. @@ -844,7 +899,7 @@ Network usage: background. `number`[] -##### withMessages +##### withMessages? `boolean` = `false` @@ -854,11 +909,37 @@ Network usage: background. *** +### apiSendMemberContactInvitation() + +> **apiSendMemberContactInvitation**(`contactId`, `message?`): `Promise`\<`Contact`\> + +Defined in: [src/api.ts:926](../src/api.ts#L926) + +Send a direct message invitation to a group member contact. +The contact must have been created with [apiCreateMemberContact](#apicreatemembercontact). +Network usage: interactive. + +#### Parameters + +##### contactId + +`number` + +##### message? + +`string` \| `MsgContent` + +#### Returns + +`Promise`\<`Contact`\> + +*** + ### apiSendMessages() -> **apiSendMessages**(`chat`, `messages`, `liveMessage`): `Promise`\<`AChatItem`[]\> +> **apiSendMessages**(`chat`, `messages`, `liveMessage?`): `Promise`\<`AChatItem`[]\> -Defined in: [src/api.ts:381](../src/api.ts#L381) +Defined in: [src/api.ts:415](../src/api.ts#L415) Send messages. Network usage: background. @@ -867,13 +948,13 @@ Network usage: background. ##### chat -`ChatInfo` | `ChatRef` | \[`ChatType`, `number`\] +`ChatInfo` \| `ChatRef` \| \[`ChatType`, `number`\] ##### messages `ComposedMessage`[] -##### liveMessage +##### liveMessage? `boolean` = `false` @@ -887,7 +968,7 @@ Network usage: background. > **apiSendTextMessage**(`chat`, `text`, `inReplyTo?`): `Promise`\<`AChatItem`[]\> -Defined in: [src/api.ts:403](../src/api.ts#L403) +Defined in: [src/api.ts:437](../src/api.ts#L437) Send text message. Network usage: background. @@ -896,7 +977,7 @@ Network usage: background. ##### chat -`ChatInfo` | `ChatRef` | \[`ChatType`, `number`\] +`ChatInfo` \| `ChatRef` \| \[`ChatType`, `number`\] ##### text @@ -916,7 +997,7 @@ Network usage: background. > **apiSendTextReply**(`chatItem`, `text`): `Promise`\<`AChatItem`[]\> -Defined in: [src/api.ts:411](../src/api.ts#L411) +Defined in: [src/api.ts:445](../src/api.ts#L445) Send text message in reply to received message. Network usage: background. @@ -941,7 +1022,7 @@ Network usage: background. > **apiSetActiveUser**(`userId`, `viewPwd?`): `Promise`\<`User`\> -Defined in: [src/api.ts:794](../src/api.ts#L794) +Defined in: [src/api.ts:869](../src/api.ts#L869) Set active user profile Network usage: no. @@ -966,7 +1047,7 @@ Network usage: no. > **apiSetAddressSettings**(`userId`, `__namedParameters`): `Promise`\<`void`\> -Defined in: [src/api.ts:364](../src/api.ts#L364) +Defined in: [src/api.ts:398](../src/api.ts#L398) Set bot address settings. Network usage: interactive. @@ -987,11 +1068,61 @@ Network usage: interactive. *** +### apiSetAutoAcceptMemberContacts() + +> **apiSetAutoAcceptMemberContacts**(`userId`, `onOff`): `Promise`\<`void`\> + +Defined in: [src/api.ts:808](../src/api.ts#L808) + +Set auto-accept member contacts. +Network usage: no. + +#### Parameters + +##### userId + +`number` + +##### onOff + +`boolean` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiSetContactCustomData() + +> **apiSetContactCustomData**(`contactId`, `customData?`): `Promise`\<`void`\> + +Defined in: [src/api.ts:798](../src/api.ts#L798) + +Set contact custom data. +Network usage: no. + +#### Parameters + +##### contactId + +`number` + +##### customData? + +`object` + +#### Returns + +`Promise`\<`void`\> + +*** + ### apiSetContactPrefs() > **apiSetContactPrefs**(`contactId`, `preferences`): `Promise`\<`void`\> -Defined in: [src/api.ts:830](../src/api.ts#L830) +Defined in: [src/api.ts:905](../src/api.ts#L905) Configure chat preference overrides for the contact. Network usage: background. @@ -1012,11 +1143,36 @@ Network usage: background. *** +### apiSetGroupCustomData() + +> **apiSetGroupCustomData**(`groupId`, `customData?`): `Promise`\<`void`\> + +Defined in: [src/api.ts:788](../src/api.ts#L788) + +Set group custom data. +Network usage: no. + +#### Parameters + +##### groupId + +`number` + +##### customData? + +`object` + +#### Returns + +`Promise`\<`void`\> + +*** + ### apiSetGroupLinkMemberRole() > **apiSetGroupLinkMemberRole**(`groupId`, `memberRole`): `Promise`\<`void`\> -Defined in: [src/api.ts:610](../src/api.ts#L610) +Defined in: [src/api.ts:644](../src/api.ts#L644) Set member role for group link. Network usage: no. @@ -1041,7 +1197,7 @@ Network usage: no. > **apiSetMembersRole**(`groupId`, `groupMemberIds`, `memberRole`): `Promise`\<`void`\> -Defined in: [src/api.ts:527](../src/api.ts#L527) +Defined in: [src/api.ts:561](../src/api.ts#L561) Set members role. Requires Admin role. Network usage: background. @@ -1070,7 +1226,7 @@ Network usage: background. > **apiSetProfileAddress**(`userId`, `enable`): `Promise`\<`UserProfileUpdateSummary`\> -Defined in: [src/api.ts:350](../src/api.ts#L350) +Defined in: [src/api.ts:384](../src/api.ts#L384) Add address to bot profile. Network usage: interactive. @@ -1095,7 +1251,7 @@ Network usage: interactive. > **apiUpdateChatItem**(`chatType`, `chatId`, `chatItemId`, `msgContent`, `liveMessage`): `Promise`\<`ChatItem`\> -Defined in: [src/api.ts:419](../src/api.ts#L419) +Defined in: [src/api.ts:453](../src/api.ts#L453) Update message. Network usage: background. @@ -1132,7 +1288,7 @@ Network usage: background. > **apiUpdateGroupProfile**(`groupId`, `groupProfile`): `Promise`\<`GroupInfo`\> -Defined in: [src/api.ts:587](../src/api.ts#L587) +Defined in: [src/api.ts:621](../src/api.ts#L621) Update group profile. Network usage: background. @@ -1157,7 +1313,7 @@ Network usage: background. > **apiUpdateProfile**(`userId`, `profile`): `Promise`\<`UserProfileUpdateSummary` \| `undefined`\> -Defined in: [src/api.ts:814](../src/api.ts#L814) +Defined in: [src/api.ts:889](../src/api.ts#L889) Update user profile. Network usage: background. @@ -1182,7 +1338,7 @@ Network usage: background. > **close**(): `Promise`\<`void`\> -Defined in: [src/api.ts:114](../src/api.ts#L114) +Defined in: [src/api.ts:148](../src/api.ts#L148) Close chat database. Usually doesn't need to be called in chat bots. @@ -1195,9 +1351,9 @@ Usually doesn't need to be called in chat bots. ### off() -> **off**\<`K`\>(`event`, `subscriber`): `void` +> **off**\<`K`\>(`event`, `subscriber?`): `void` -Defined in: [src/api.ts:253](../src/api.ts#L253) +Defined in: [src/api.ts:287](../src/api.ts#L287) Unsubscribe all or a specific handler from a specific event. @@ -1215,12 +1371,12 @@ Unsubscribe all or a specific handler from a specific event. The event type to unsubscribe from. -##### subscriber +##### subscriber? + +[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`K`\> \| `undefined` An optional subscriber function for the event. -[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`K`\> | `undefined` - #### Returns `void` @@ -1229,20 +1385,20 @@ An optional subscriber function for the event. ### offAny() -> **offAny**(`receiver`): `void` +> **offAny**(`receiver?`): `void` -Defined in: [src/api.ts:269](../src/api.ts#L269) +Defined in: [src/api.ts:303](../src/api.ts#L303) Unsubscribe all or a specific handler from any events. #### Parameters -##### receiver +##### receiver? + +[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`Tag`\> \| `undefined` An optional subscriber function for the event. -[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`Tag`\> | `undefined` - #### Returns `void` @@ -1255,7 +1411,7 @@ An optional subscriber function for the event. > **on**\<`K`\>(`subscribers`): `void` -Defined in: [src/api.ts:163](../src/api.ts#L163) +Defined in: [src/api.ts:197](../src/api.ts#L197) Subscribe multiple event handlers at once. @@ -1285,7 +1441,7 @@ If the same function is subscribed to event. > **on**\<`K`\>(`event`, `subscriber`): `void` -Defined in: [src/api.ts:171](../src/api.ts#L171) +Defined in: [src/api.ts:205](../src/api.ts#L205) Subscribe a handler to a specific event. @@ -1323,7 +1479,7 @@ If the same function is subscribed to event. > **onAny**(`receiver`): `void` -Defined in: [src/api.ts:194](../src/api.ts#L194) +Defined in: [src/api.ts:228](../src/api.ts#L228) Subscribe a handler to any event. @@ -1349,7 +1505,7 @@ If the same function is subscribed to event. > **once**\<`K`\>(`event`, `subscriber`): `void` -Defined in: [src/api.ts:205](../src/api.ts#L205) +Defined in: [src/api.ts:239](../src/api.ts#L239) Subscribe a handler to a specific event to be delivered one time. @@ -1385,13 +1541,13 @@ If the same function is subscribed to event. ### recvChatEvent() -> **recvChatEvent**(`wait`): `Promise`\<`ChatEvent` \| `undefined`\> +> **recvChatEvent**(`wait?`): `Promise`\<`ChatEvent` \| `undefined`\> -Defined in: [src/api.ts:304](../src/api.ts#L304) +Defined in: [src/api.ts:338](../src/api.ts#L338) #### Parameters -##### wait +##### wait? `number` = `5_000_000` @@ -1405,7 +1561,7 @@ Defined in: [src/api.ts:304](../src/api.ts#L304) > **sendChatCmd**(`cmd`): `Promise`\<`ChatResponse`\> -Defined in: [src/api.ts:300](../src/api.ts#L300) +Defined in: [src/api.ts:334](../src/api.ts#L334) #### Parameters @@ -1423,7 +1579,7 @@ Defined in: [src/api.ts:300](../src/api.ts#L300) > **startChat**(): `Promise`\<`void`\> -Defined in: [src/api.ts:88](../src/api.ts#L88) +Defined in: [src/api.ts:122](../src/api.ts#L122) Start chat controller. Must be called with the existing user profile. @@ -1437,7 +1593,7 @@ Start chat controller. Must be called with the existing user profile. > **stopChat**(): `Promise`\<`void`\> -Defined in: [src/api.ts:102](../src/api.ts#L102) +Defined in: [src/api.ts:136](../src/api.ts#L136) Stop chat controller. Must be called before closing the database. @@ -1453,9 +1609,9 @@ Usually doesn't need to be called in chat bots. #### Call Signature -> **wait**\<`K`\>(`event`): `Promise`\<`ChatEvent` & \{ `type`: `K`; \}\> +> **wait**\<`K`\>(`event`): `Promise`\<`ChatEvent` & `object`\> -Defined in: [src/api.ts:213](../src/api.ts#L213) +Defined in: [src/api.ts:247](../src/api.ts#L247) Waits for specific event, with an optional predicate. Returns `undefined` on timeout if specified. @@ -1474,13 +1630,13 @@ Returns `undefined` on timeout if specified. ##### Returns -`Promise`\<`ChatEvent` & \{ `type`: `K`; \}\> +`Promise`\<`ChatEvent` & `object`\> #### Call Signature -> **wait**\<`K`\>(`event`, `predicate`): `Promise`\<`ChatEvent` & \{ `type`: `K`; \}\> +> **wait**\<`K`\>(`event`, `predicate`): `Promise`\<`ChatEvent` & `object`\> -Defined in: [src/api.ts:214](../src/api.ts#L214) +Defined in: [src/api.ts:248](../src/api.ts#L248) Waits for specific event, with an optional predicate. Returns `undefined` on timeout if specified. @@ -1499,17 +1655,17 @@ Returns `undefined` on timeout if specified. ###### predicate -(`event`) => `boolean` | `undefined` +((`event`) => `boolean`) \| `undefined` ##### Returns -`Promise`\<`ChatEvent` & \{ `type`: `K`; \}\> +`Promise`\<`ChatEvent` & `object`\> #### Call Signature > **wait**\<`K`\>(`event`, `timeout`): `Promise`\ -Defined in: [src/api.ts:215](../src/api.ts#L215) +Defined in: [src/api.ts:249](../src/api.ts#L249) Waits for specific event, with an optional predicate. Returns `undefined` on timeout if specified. @@ -1538,7 +1694,7 @@ Returns `undefined` on timeout if specified. > **wait**\<`K`\>(`event`, `predicate`, `timeout`): `Promise`\ -Defined in: [src/api.ts:216](../src/api.ts#L216) +Defined in: [src/api.ts:250](../src/api.ts#L250) Waits for specific event, with an optional predicate. Returns `undefined` on timeout if specified. @@ -1557,7 +1713,7 @@ Returns `undefined` on timeout if specified. ###### predicate -(`event`) => `boolean` | `undefined` +((`event`) => `boolean`) \| `undefined` ###### timeout @@ -1571,25 +1727,19 @@ Returns `undefined` on timeout if specified. ### init() -> `static` **init**(`dbFilePrefix`, `dbKey?`, `confirm?`): `Promise`\<`ChatApi`\> +> `static` **init**(`db`, `confirm?`): `Promise`\<`ChatApi`\> -Defined in: [src/api.ts:76](../src/api.ts#L76) +Defined in: [src/api.ts:110](../src/api.ts#L110) Initializes the ChatApi. #### Parameters -##### dbFilePrefix +##### db -`string` +[`DbConfig`](api.TypeAlias.DbConfig.md) -File prefix for the database files. - -##### dbKey? - -`string` = `""` - -Database encryption key. +Database configuration (sqlite or postgres). ##### confirm? diff --git a/packages/simplex-chat-nodejs/docs/api.Class.ChatCommandError.md b/packages/simplex-chat-nodejs/docs/api.Class.ChatCommandError.md index a4955cb3d9..5ec1d40540 100644 --- a/packages/simplex-chat-nodejs/docs/api.Class.ChatCommandError.md +++ b/packages/simplex-chat-nodejs/docs/api.Class.ChatCommandError.md @@ -74,7 +74,7 @@ Defined in: [src/api.ts:6](../src/api.ts#L6) ### stack? -> `optional` **stack**: `string` +> `optional` **stack?**: `string` Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1078](../node_modules/typescript/lib/lib.es5.d.ts#L1078) diff --git a/packages/simplex-chat-nodejs/docs/api.Interface.BotAddressSettings.md b/packages/simplex-chat-nodejs/docs/api.Interface.BotAddressSettings.md index efb4a75e81..23c5e35326 100644 --- a/packages/simplex-chat-nodejs/docs/api.Interface.BotAddressSettings.md +++ b/packages/simplex-chat-nodejs/docs/api.Interface.BotAddressSettings.md @@ -14,7 +14,7 @@ Bot address settings. ### autoAccept? -> `optional` **autoAccept**: `boolean` +> `optional` **autoAccept?**: `boolean` Defined in: [src/api.ts:28](../src/api.ts#L28) @@ -30,7 +30,7 @@ true ### businessAddress? -> `optional` **businessAddress**: `boolean` +> `optional` **businessAddress?**: `boolean` Defined in: [src/api.ts:41](../src/api.ts#L41) @@ -47,7 +47,7 @@ false ### welcomeMessage? -> `optional` **welcomeMessage**: `string` \| `MsgContent` +> `optional` **welcomeMessage?**: `string` \| `MsgContent` Defined in: [src/api.ts:34](../src/api.ts#L34) diff --git a/packages/simplex-chat-nodejs/docs/api.TypeAlias.DbConfig.md b/packages/simplex-chat-nodejs/docs/api.TypeAlias.DbConfig.md new file mode 100644 index 0000000000..7fe255327c --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.TypeAlias.DbConfig.md @@ -0,0 +1,64 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / DbConfig + +# Type Alias: DbConfig + +> **DbConfig** = \{ `encryptionKey?`: `string`; `filePrefix`: `string`; `type`: `"sqlite"`; \} \| \{ `connectionString`: `string`; `schemaPrefix?`: `string`; `type`: `"postgres"`; \} + +Defined in: [src/api.ts:65](../src/api.ts#L65) + +Database configuration. The native library is built against exactly one +backend (see `simplex_backend` / `SIMPLEX_BACKEND` at install time); this +type makes the caller state which one they are targeting so field names +can't lie about their meaning. + +## Union Members + +### Type Literal + +\{ `encryptionKey?`: `string`; `filePrefix`: `string`; `type`: `"sqlite"`; \} + +#### encryptionKey? + +> `optional` **encryptionKey?**: `string` + +Optional SQLCipher encryption key. Empty/omitted = unencrypted. + +#### filePrefix + +> **filePrefix**: `string` + +File prefix — two schema files are named `_chat.db` and `_agent.db`. + +#### type + +> **type**: `"sqlite"` + +SQLite backend (default). + +*** + +### Type Literal + +\{ `connectionString`: `string`; `schemaPrefix?`: `string`; `type`: `"postgres"`; \} + +#### connectionString + +> **connectionString**: `string` + +PostgreSQL connection string (e.g. `postgres://user:pass@host/db`). + +#### schemaPrefix? + +> `optional` **schemaPrefix?**: `string` + +Schema prefix used to namespace tables. Defaults to `"simplex_v1"` when omitted. + +#### type + +> **type**: `"postgres"` + +PostgreSQL backend (Linux x86_64 only, libpq5 required). diff --git a/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md index 6197befc8a..bf40b3165c 100644 --- a/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md +++ b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md @@ -4,7 +4,7 @@ [simplex-chat](README.md) / [api](Namespace.api.md) / EventSubscriberFunc -# Type Alias: EventSubscriberFunc()\ +# Type Alias: EventSubscriberFunc\ > **EventSubscriberFunc**\<`K`\> = (`event`) => `void` \| `Promise`\<`void`\> @@ -20,7 +20,7 @@ Defined in: [src/api.ts:50](../src/api.ts#L50) ### event -`ChatEvent` & \{ `type`: `K`; \} +`ChatEvent` & `object` ## Returns diff --git a/packages/simplex-chat-nodejs/docs/bot.Function.run.md b/packages/simplex-chat-nodejs/docs/bot.Function.run.md index bc31ad01a8..3c33e6c7d4 100644 --- a/packages/simplex-chat-nodejs/docs/bot.Function.run.md +++ b/packages/simplex-chat-nodejs/docs/bot.Function.run.md @@ -8,7 +8,7 @@ > **run**(`__namedParameters`): `Promise`\<\[[`ChatApi`](api.Class.ChatApi.md), `User`, `UserContactLink` \| `undefined`\]\> -Defined in: [src/bot.ts:49](../src/bot.ts#L49) +Defined in: [src/bot.ts:47](../src/bot.ts#L47) ## Parameters diff --git a/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md b/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md index 0951c5a129..4624b1608b 100644 --- a/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md +++ b/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md @@ -6,43 +6,43 @@ # Interface: BotConfig -Defined in: [src/bot.ts:37](../src/bot.ts#L37) +Defined in: [src/bot.ts:35](../src/bot.ts#L35) ## Properties ### dbOpts -> **dbOpts**: [`BotDbOpts`](bot.Interface.BotDbOpts.md) +> **dbOpts**: [`BotDbOpts`](bot.TypeAlias.BotDbOpts.md) -Defined in: [src/bot.ts:39](../src/bot.ts#L39) +Defined in: [src/bot.ts:37](../src/bot.ts#L37) *** ### events? -> `optional` **events**: [`EventSubscribers`](api.TypeAlias.EventSubscribers.md) +> `optional` **events?**: [`EventSubscribers`](api.TypeAlias.EventSubscribers.md) -Defined in: [src/bot.ts:46](../src/bot.ts#L46) +Defined in: [src/bot.ts:44](../src/bot.ts#L44) *** ### onCommands? -> `optional` **onCommands**: \{\[`key`: `string`\]: (`chatItem`, `command`) => `void` \| `Promise`\<`void`\> \| `undefined`; \} +> `optional` **onCommands?**: `object` -Defined in: [src/bot.ts:43](../src/bot.ts#L43) +Defined in: [src/bot.ts:41](../src/bot.ts#L41) #### Index Signature -\[`key`: `string`\]: (`chatItem`, `command`) => `void` \| `Promise`\<`void`\> \| `undefined` +\[`key`: `string`\]: ((`chatItem`, `command`) => `void` \| `Promise`\<`void`\>) \| `undefined` *** -### onMessage()? +### onMessage? -> `optional` **onMessage**: (`chatItem`, `content`) => `void` \| `Promise`\<`void`\> +> `optional` **onMessage?**: (`chatItem`, `content`) => `void` \| `Promise`\<`void`\> -Defined in: [src/bot.ts:41](../src/bot.ts#L41) +Defined in: [src/bot.ts:39](../src/bot.ts#L39) #### Parameters @@ -64,7 +64,7 @@ Defined in: [src/bot.ts:41](../src/bot.ts#L41) > **options**: [`BotOptions`](bot.Interface.BotOptions.md) -Defined in: [src/bot.ts:40](../src/bot.ts#L40) +Defined in: [src/bot.ts:38](../src/bot.ts#L38) *** @@ -72,4 +72,4 @@ Defined in: [src/bot.ts:40](../src/bot.ts#L40) > **profile**: `Profile` -Defined in: [src/bot.ts:38](../src/bot.ts#L38) +Defined in: [src/bot.ts:36](../src/bot.ts#L36) diff --git a/packages/simplex-chat-nodejs/docs/bot.Interface.BotDbOpts.md b/packages/simplex-chat-nodejs/docs/bot.Interface.BotDbOpts.md deleted file mode 100644 index 7a9f113f6a..0000000000 --- a/packages/simplex-chat-nodejs/docs/bot.Interface.BotDbOpts.md +++ /dev/null @@ -1,33 +0,0 @@ -[**simplex-chat**](README.md) - -*** - -[simplex-chat](README.md) / [bot](Namespace.bot.md) / BotDbOpts - -# Interface: BotDbOpts - -Defined in: [src/bot.ts:7](../src/bot.ts#L7) - -## Properties - -### confirmMigrations? - -> `optional` **confirmMigrations**: [`MigrationConfirmation`](core.Enumeration.MigrationConfirmation.md) - -Defined in: [src/bot.ts:10](../src/bot.ts#L10) - -*** - -### dbFilePrefix - -> **dbFilePrefix**: `string` - -Defined in: [src/bot.ts:8](../src/bot.ts#L8) - -*** - -### dbKey? - -> `optional` **dbKey**: `string` - -Defined in: [src/bot.ts:9](../src/bot.ts#L9) diff --git a/packages/simplex-chat-nodejs/docs/bot.Interface.BotOptions.md b/packages/simplex-chat-nodejs/docs/bot.Interface.BotOptions.md index 44d4380e5a..eee56b879a 100644 --- a/packages/simplex-chat-nodejs/docs/bot.Interface.BotOptions.md +++ b/packages/simplex-chat-nodejs/docs/bot.Interface.BotOptions.md @@ -6,76 +6,76 @@ # Interface: BotOptions -Defined in: [src/bot.ts:13](../src/bot.ts#L13) +Defined in: [src/bot.ts:11](../src/bot.ts#L11) ## Properties ### addressSettings? -> `optional` **addressSettings**: [`BotAddressSettings`](api.Interface.BotAddressSettings.md) - -Defined in: [src/bot.ts:17](../src/bot.ts#L17) - -*** - -### allowFiles? - -> `optional` **allowFiles**: `boolean` - -Defined in: [src/bot.ts:18](../src/bot.ts#L18) - -*** - -### commands? - -> `optional` **commands**: `ChatBotCommand`[] - -Defined in: [src/bot.ts:19](../src/bot.ts#L19) - -*** - -### createAddress? - -> `optional` **createAddress**: `boolean` - -Defined in: [src/bot.ts:14](../src/bot.ts#L14) - -*** - -### logContacts? - -> `optional` **logContacts**: `boolean` - -Defined in: [src/bot.ts:21](../src/bot.ts#L21) - -*** - -### logNetwork? - -> `optional` **logNetwork**: `boolean` - -Defined in: [src/bot.ts:22](../src/bot.ts#L22) - -*** - -### updateAddress? - -> `optional` **updateAddress**: `boolean` +> `optional` **addressSettings?**: [`BotAddressSettings`](api.Interface.BotAddressSettings.md) Defined in: [src/bot.ts:15](../src/bot.ts#L15) *** -### updateProfile? +### allowFiles? -> `optional` **updateProfile**: `boolean` +> `optional` **allowFiles?**: `boolean` Defined in: [src/bot.ts:16](../src/bot.ts#L16) *** -### useBotProfile? +### commands? -> `optional` **useBotProfile**: `boolean` +> `optional` **commands?**: `ChatBotCommand`[] + +Defined in: [src/bot.ts:17](../src/bot.ts#L17) + +*** + +### createAddress? + +> `optional` **createAddress?**: `boolean` + +Defined in: [src/bot.ts:12](../src/bot.ts#L12) + +*** + +### logContacts? + +> `optional` **logContacts?**: `boolean` + +Defined in: [src/bot.ts:19](../src/bot.ts#L19) + +*** + +### logNetwork? + +> `optional` **logNetwork?**: `boolean` Defined in: [src/bot.ts:20](../src/bot.ts#L20) + +*** + +### updateAddress? + +> `optional` **updateAddress?**: `boolean` + +Defined in: [src/bot.ts:13](../src/bot.ts#L13) + +*** + +### updateProfile? + +> `optional` **updateProfile?**: `boolean` + +Defined in: [src/bot.ts:14](../src/bot.ts#L14) + +*** + +### useBotProfile? + +> `optional` **useBotProfile?**: `boolean` + +Defined in: [src/bot.ts:18](../src/bot.ts#L18) diff --git a/packages/simplex-chat-nodejs/docs/bot.TypeAlias.BotDbOpts.md b/packages/simplex-chat-nodejs/docs/bot.TypeAlias.BotDbOpts.md new file mode 100644 index 0000000000..b035f41355 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/bot.TypeAlias.BotDbOpts.md @@ -0,0 +1,17 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [bot](Namespace.bot.md) / BotDbOpts + +# Type Alias: BotDbOpts + +> **BotDbOpts** = [`DbConfig`](api.TypeAlias.DbConfig.md) & `object` + +Defined in: [src/bot.ts:7](../src/bot.ts#L7) + +## Type Declaration + +### confirmMigrations? + +> `optional` **confirmMigrations?**: [`MigrationConfirmation`](core.Enumeration.MigrationConfirmation.md) diff --git a/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md b/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md index c6082f2985..5bd0722f0c 100644 --- a/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md +++ b/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md @@ -16,7 +16,7 @@ Defined in: [src/core.ts:92](../src/core.ts#L92) ### Constructor -> **new ChatAPIError**(`message`, `chatError`): `ChatAPIError` +> **new ChatAPIError**(`message`, `chatError?`): `ChatAPIError` Defined in: [src/core.ts:93](../src/core.ts#L93) @@ -26,9 +26,9 @@ Defined in: [src/core.ts:93](../src/core.ts#L93) `string` -##### chatError +##### chatError? -`ChatError` | `undefined` +`ChatError` \| `undefined` #### Returns @@ -74,7 +74,7 @@ Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1076](../node_modules/typ ### stack? -> `optional` **stack**: `string` +> `optional` **stack?**: `string` Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1078](../node_modules/typescript/lib/lib.es5.d.ts#L1078) diff --git a/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md b/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md index eff5123fb0..0feceae4fd 100644 --- a/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md +++ b/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md @@ -74,7 +74,7 @@ Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1076](../node_modules/typ ### stack? -> `optional` **stack**: `string` +> `optional` **stack?**: `string` Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1078](../node_modules/typescript/lib/lib.es5.d.ts#L1078) diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md b/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md index 906ef3ec3e..8d18997ec4 100644 --- a/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md +++ b/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md @@ -18,7 +18,7 @@ Defined in: [src/core.ts:87](../src/core.ts#L87) ### error? -> `optional` **error**: `ChatError` +> `optional` **error?**: `ChatError` Defined in: [src/core.ts:89](../src/core.ts#L89) @@ -26,6 +26,6 @@ Defined in: [src/core.ts:89](../src/core.ts#L89) ### result? -> `optional` **result**: `R` +> `optional` **result?**: `R` Defined in: [src/core.ts:88](../src/core.ts#L88) diff --git a/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js b/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js index 16d0678b64..8899e9bf15 100644 --- a/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js +++ b/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js @@ -2,7 +2,7 @@ const {bot} = await import("../dist/index.js") const [chat, _user, _address] = await bot.run({ profile: {displayName: "Squaring bot example", fullName: ""}, - dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""}, + dbOpts: {type: "sqlite", filePrefix: "./squaring_bot"}, options: { addressSettings: {welcomeMessage: "Send a number, I will square it."}, }, diff --git a/packages/simplex-chat-nodejs/examples/squaring-bot.ts b/packages/simplex-chat-nodejs/examples/squaring-bot.ts index 682e7b887a..dbb58d90dc 100644 --- a/packages/simplex-chat-nodejs/examples/squaring-bot.ts +++ b/packages/simplex-chat-nodejs/examples/squaring-bot.ts @@ -5,7 +5,7 @@ import {bot, util} from "../dist" const welcomeMessage = "Hello! I am a simple squaring bot.\n\nIf you send me a number, I will calculate its square." const [chat, _user, _address] = await bot.run({ profile: {displayName: "Squaring bot example", fullName: ""}, - dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""}, + dbOpts: {type: "sqlite", filePrefix: "./squaring_bot"}, options: { addressSettings: {autoAccept: true, welcomeMessage, businessAddress: false}, commands: [ // commands to show in client UI diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json index 498d502edd..c5cc255722 100644 --- a/packages/simplex-chat-nodejs/package.json +++ b/packages/simplex-chat-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "simplex-chat", - "version": "6.5.0-beta.4.4", + "version": "6.5.1", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ @@ -24,7 +24,7 @@ "docs": "typedoc" }, "dependencies": { - "@simplex-chat/types": "^0.3.0", + "@simplex-chat/types": "^0.6.0", "extract-zip": "^2.0.1", "fast-deep-equal": "^3.1.3", "node-addon-api": "^8.5.0" diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts index c3e85b3915..0d3339df9a 100644 --- a/packages/simplex-chat-nodejs/src/api.ts +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -56,6 +56,41 @@ interface EventSubscriber { once: boolean } +/** + * Database configuration. The native library is built against exactly one + * backend (see `simplex_backend` / `SIMPLEX_BACKEND` at install time); this + * type makes the caller state which one they are targeting so field names + * can't lie about their meaning. + */ +export type DbConfig = + | { + /** SQLite backend (default). */ + type: "sqlite" + /** File prefix — two schema files are named `_chat.db` and `_agent.db`. */ + filePrefix: string + /** Optional SQLCipher encryption key. Empty/omitted = unencrypted. */ + encryptionKey?: string + } + | { + /** PostgreSQL backend (Linux x86_64 only, libpq5 required). */ + type: "postgres" + /** Schema prefix used to namespace tables. Defaults to `"simplex_v1"` when omitted. */ + schemaPrefix?: string + /** PostgreSQL connection string (e.g. `postgres://user:pass@host/db`). */ + connectionString: string + } + +function dbConfigToMigrateArgs(db: DbConfig): [string, string] { + switch (db.type) { + case "sqlite": + return [db.filePrefix, db.encryptionKey ?? ""] + case "postgres": + return [db.schemaPrefix ?? "", db.connectionString] + default: + throw new Error(`Invalid DbConfig: ${JSON.stringify(db satisfies never)}`) + } +} + /** * Main API class for interacting with the chat core library. */ @@ -64,21 +99,20 @@ export class ChatApi { private eventsLoop: Promise | undefined = undefined private subscribers: {[K in CEvt.Tag]?: EventSubscriber[]} = {} private receivers: EventSubscriberFunc[] = [] - + private constructor(protected ctrl_: bigint | undefined) {} /** * Initializes the ChatApi. - * @param {string} dbFilePrefix - File prefix for the database files. - * @param {string} [dbKey=""] - Database encryption key. + * @param {DbConfig} db - Database configuration (sqlite or postgres). * @param {core.MigrationConfirmation} [confirm=core.MigrationConfirmation.YesUp] - Migration confirmation mode. */ static async init( - dbFilePrefix: string, - dbKey: string = "", + db: DbConfig, confirm = core.MigrationConfirmation.YesUp ): Promise { - const ctrl = await core.chatMigrateInit(dbFilePrefix, dbKey, confirm) + const [path, key] = dbConfigToMigrateArgs(db) + const ctrl = await core.chatMigrateInit(path, key, confirm) return new ChatApi(ctrl) } @@ -654,7 +688,7 @@ export class ChatApi { * Network usage: interactive. */ async apiConnectPlan(userId: number, connectionLink: string): Promise<[T.ConnectionPlan, T.CreatedConnLink]> { - const r = await this.sendChatCmd(CC.APIConnectPlan.cmdString({userId, connectionLink})) + const r = await this.sendChatCmd(CC.APIConnectPlan.cmdString({userId, connectionLink, resolveKnown: false})) if (r.type === "connectionPlan") return [r.connectionPlan, r.connLink] throw new ChatCommandError("error getting connect plan", r) } @@ -730,6 +764,25 @@ export class ChatApi { throw new ChatCommandError("error listing groups", r) } + /** + * Get chat previews (paginated). + * Network usage: no. + * + * Prefer this over apiListContacts / apiListGroups for any scan: those + * methods load every record into memory in a single response and will fail + * on large databases. + */ + async apiGetChats( + userId: number, + pagination: T.PaginationByTime, + query: T.ChatListQuery = {type: "filters", favorite: false, unread: false}, + pendingConnections = false, + ): Promise { + const r = await this.sendChatCmd(CC.APIGetChats.cmdString({userId, pendingConnections, pagination, query})) + if (r.type === "apiChats") return r.chats + throw new ChatCommandError("error getting chats", r) + } + /** * Delete chat. * Network usage: background. @@ -813,7 +866,7 @@ export class ChatApi { * Network usage: no. */ async apiCreateActiveUser(profile?: T.Profile): Promise { - const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false}})) + const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false, userChatRelay: false}})) if (r.type === "activeUser") return r.user throw new ChatCommandError("unexpected response", r) } @@ -872,4 +925,34 @@ export class ChatApi { const r = await this.sendChatCmd(CC.APISetContactPrefs.cmdString({contactId, preferences})) if (r.type !== "contactPrefsUpdated") throw new ChatCommandError("error setting contact prefs", r) } + + /** + * Create a direct message contact with a group member. + * Returns the created contact. + * Network usage: interactive. + */ + async apiCreateMemberContact(groupId: number, groupMemberId: number): Promise { + const r: any = await this.sendChatCmd(`/_create member contact #${groupId} ${groupMemberId}`) + if (r.type === "newMemberContact") return r.contact + throw new ChatCommandError("error creating member contact", r) + } + + /** + * Send a direct message invitation to a group member contact. + * The contact must have been created with {@link apiCreateMemberContact}. + * Network usage: interactive. + */ + async apiSendMemberContactInvitation(contactId: number, message?: T.MsgContent | string): Promise { + let cmd = `/_invite member contact @${contactId}` + if (message !== undefined) { + if (typeof message === "string") { + cmd += ` text ${message}` + } else { + cmd += ` json ${JSON.stringify(message)}` + } + } + const r: any = await this.sendChatCmd(cmd) + if (r.type === "newMemberContactSentInv") return r.contact + throw new ChatCommandError("error sending member contact invitation", r) + } } diff --git a/packages/simplex-chat-nodejs/src/bot.ts b/packages/simplex-chat-nodejs/src/bot.ts index 95a0c13d96..f6cb753d27 100644 --- a/packages/simplex-chat-nodejs/src/bot.ts +++ b/packages/simplex-chat-nodejs/src/bot.ts @@ -4,9 +4,7 @@ import * as core from "./core" import * as util from "./util" import equal = require("fast-deep-equal") -export interface BotDbOpts { - dbFilePrefix: string // two schema files will be named _chat.db and _agent.db - dbKey?: string +export type BotDbOpts = api.DbConfig & { confirmMigrations?: core.MigrationConfirmation } @@ -47,7 +45,7 @@ export interface BotConfig { } export async function run({profile, dbOpts, options = defaultOpts, onMessage, onCommands = {}, events = {}}: BotConfig): Promise<[api.ChatApi, T.User, T.UserContactLink | undefined]> { - const bot = await api.ChatApi.init(dbOpts.dbFilePrefix, dbOpts.dbKey || "", dbOpts.confirmMigrations || core.MigrationConfirmation.YesUp) + const bot = await api.ChatApi.init(dbOpts, dbOpts.confirmMigrations || core.MigrationConfirmation.YesUp) const opts = fullOptions(options) if (onMessage) subscribeMessages(bot, onMessage) if (Object.keys(onCommands).length > 0) subscribeCommands(bot, onCommands) diff --git a/packages/simplex-chat-nodejs/src/download-libs.js b/packages/simplex-chat-nodejs/src/download-libs.js index 6b8f583155..5c1b70cda0 100644 --- a/packages/simplex-chat-nodejs/src/download-libs.js +++ b/packages/simplex-chat-nodejs/src/download-libs.js @@ -4,7 +4,19 @@ const path = require('path'); const extract = require('extract-zip'); const GITHUB_REPO = 'simplex-chat/simplex-chat-libs'; -const RELEASE_TAG = 'v6.5.0-beta.4'; +const RELEASE_TAG = 'v6.5.1'; +const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase(); + +if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') { + console.error(`✗ Invalid SIMPLEX_BACKEND: "${BACKEND}". Must be "sqlite" or "postgres".`); + process.exit(1); +} + +if (BACKEND === 'postgres' && (process.platform !== 'linux' || process.arch !== 'x64')) { + console.error(`✗ SIMPLEX_BACKEND=postgres is only supported on Linux x86_64.`); + process.exit(1); +} + const ROOT_DIR = process.cwd(); // Root of the package being installed const LIBS_DIR = path.join(ROOT_DIR, 'libs') const INSTALLED_FILE = path.join(LIBS_DIR, 'installed.txt'); @@ -56,11 +68,12 @@ function isAlreadyInstalled() { try { const installedVersion = fs.readFileSync(INSTALLED_FILE, 'utf-8').trim(); - if (installedVersion === RELEASE_TAG) { - console.log(`✓ Libraries version ${RELEASE_TAG} already installed`); + const expectedVersion = `${RELEASE_TAG}:${BACKEND}`; + if (installedVersion === expectedVersion) { + console.log(`✓ Libraries version ${RELEASE_TAG}:${BACKEND} already installed`); return true; } else { - console.log(`Version mismatch: installed ${installedVersion}, need ${RELEASE_TAG}`); + console.log(`Version mismatch: installed ${installedVersion}, need ${expectedVersion}`); cleanLibsDirectory(); return false; } @@ -79,12 +92,14 @@ async function install() { const { platformName, archName } = getPlatformInfo(); const repoName = GITHUB_REPO.split('/')[1]; - const zipFilename = `${repoName}-${platformName}-${archName}.zip`; + const backendSuffix = BACKEND === 'postgres' ? '-postgres' : ''; + const zipFilename = `${repoName}-${platformName}-${archName}${backendSuffix}.zip`; const ZIP_URL = `https://github.com/${GITHUB_REPO}/releases/download/${RELEASE_TAG}/${zipFilename}`; const ZIP_PATH = path.join(ROOT_DIR, zipFilename); const TEMP_EXTRACT_DIR = path.join(ROOT_DIR, '.temp-extract'); console.log(`Detected: ${platformName} ${archName}`); + console.log(`Backend: ${BACKEND}`); console.log(`Downloading: ${zipFilename}`); // Create libs directory @@ -124,8 +139,8 @@ async function install() { } // Write installed.txt with version - fs.writeFileSync(INSTALLED_FILE, RELEASE_TAG, 'utf-8'); - console.log(`✓ Wrote version ${RELEASE_TAG} to installed.txt`); + fs.writeFileSync(INSTALLED_FILE, `${RELEASE_TAG}:${BACKEND}`, 'utf-8'); + console.log(`✓ Wrote version ${RELEASE_TAG}:${BACKEND} to installed.txt`); // Cleanup fs.rmSync(TEMP_EXTRACT_DIR, { recursive: true, force: true }); diff --git a/packages/simplex-chat-nodejs/tests/api.test.ts b/packages/simplex-chat-nodejs/tests/api.test.ts index 52153ecfed..99d511371c 100644 --- a/packages/simplex-chat-nodejs/tests/api.test.ts +++ b/packages/simplex-chat-nodejs/tests/api.test.ts @@ -15,8 +15,8 @@ describe("API tests (use preset servers)", () => { it("should send/receive message", async () => { // create users and start chat controllers - const alice = await api.ChatApi.init(alicePath) - const bob = await api.ChatApi.init(bobPath) + const alice = await api.ChatApi.init({type: "sqlite", filePrefix: alicePath}) + const bob = await api.ChatApi.init({type: "sqlite", filePrefix: bobPath}) const servers: string[] = [] let eventCount = 0 alice.on("hostConnected" as CEvt.Tag, async ({transportHost}: any) => { servers.push(transportHost) }) @@ -64,4 +64,89 @@ describe("API tests (use preset servers)", () => { expect(servers[0] !== servers[1]).toBe(true) expect(eventCount > 0).toBe(true) }, 30000) + + it("should create member contact and send invitation", async () => { + // create 3 users and start chat controllers + const alice = await api.ChatApi.init({type: "sqlite", filePrefix: alicePath}) + const bob = await api.ChatApi.init({type: "sqlite", filePrefix: bobPath}) + const carolPath = path.join(tmpDir, "carol") + const carol = await api.ChatApi.init({type: "sqlite", filePrefix: carolPath}) + const aliceUser = await alice.apiCreateActiveUser({displayName: "alice", fullName: ""}) + await bob.apiCreateActiveUser({displayName: "bob", fullName: ""}) + await carol.apiCreateActiveUser({displayName: "carol", fullName: ""}) + await alice.startChat() + await bob.startChat() + await carol.startChat() + // connect alice <-> bob + const aliceLink1 = await alice.apiCreateLink(aliceUser.userId) + await expect(bob.apiConnectActiveUser(aliceLink1)).resolves.toBe(api.ConnReqType.Invitation) + const [bobContact] = await Promise.all([ + (await alice.wait("contactConnected")).contact, + (await bob.wait("contactConnected")).contact + ]) + // connect alice <-> carol + const aliceLink2 = await alice.apiCreateLink(aliceUser.userId) + await expect(carol.apiConnectActiveUser(aliceLink2)).resolves.toBe(api.ConnReqType.Invitation) + const [carolContact] = await Promise.all([ + (await alice.wait("contactConnected")).contact, + (await carol.wait("contactConnected")).contact + ]) + // create group with direct messages enabled + const group = await alice.apiNewGroup(aliceUser.userId, { + displayName: "test-group", + fullName: "", + groupPreferences: { + directMessages: {enable: T.GroupFeatureEnabled.On}, + }, + }) + const groupId = group.groupId + // add bob to the group + const bobInvP = bob.wait("receivedGroupInvitation", 15000) + await alice.apiAddMember(groupId, bobContact.contactId, T.GroupMemberRole.Member) + const bobInvEvt = await bobInvP + expect(bobInvEvt).toBeDefined() + const aliceBobConnP = alice.wait("connectedToGroupMember", 15000) + const bobAliceConnP = bob.wait("connectedToGroupMember", 15000) + await bob.apiJoinGroup(bobInvEvt!.groupInfo.groupId) + await Promise.all([aliceBobConnP, bobAliceConnP]) + // add carol to the group + const carolInvP = carol.wait("receivedGroupInvitation", 30000) + await alice.apiAddMember(groupId, carolContact.contactId, T.GroupMemberRole.Member) + const carolInvEvt = await carolInvP + expect(carolInvEvt).toBeDefined() + // wait for carol to connect to both alice and bob (and vice versa) + const bobCarolConnP = bob.wait("connectedToGroupMember", + (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "carol", 30000) + const carolAliceConnP = carol.wait("connectedToGroupMember", + (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "alice", 30000) + const carolBobConnP = carol.wait("connectedToGroupMember", + (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "bob", 30000) + const aliceCarolConnP = alice.wait("connectedToGroupMember", + (evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "carol", 30000) + await carol.apiJoinGroup(carolInvEvt!.groupInfo.groupId) + await Promise.all([bobCarolConnP, carolAliceConnP, carolBobConnP, aliceCarolConnP]) + // find carol's memberId from bob's perspective + const members = await bob.apiListMembers(groupId) + const carolMember = members.find(m => m.memberProfile.displayName === "carol") + expect(carolMember).toBeDefined() + // test apiCreateMemberContact + const dmContact = await bob.apiCreateMemberContact(groupId, carolMember!.groupMemberId) + expect(dmContact).toBeDefined() + expect(dmContact.contactId).toBeDefined() + // test apiSendMemberContactInvitation + const carolDmP = carol.wait("newMemberContactReceivedInv" as CEvt.Tag, 30000) + const invContact = await bob.apiSendMemberContactInvitation(dmContact.contactId, "hello from bob") + expect(invContact).toBeDefined() + // carol should receive the member contact invitation + const carolDmEvt = await carolDmP + expect(carolDmEvt).toBeDefined() + expect((carolDmEvt as any).contact).toBeDefined() + // cleanup + await alice.stopChat() + await bob.stopChat() + await carol.stopChat() + await alice.close() + await bob.close() + await carol.close() + }, 90000) }) diff --git a/packages/simplex-chat-nodejs/tests/bot.test.ts b/packages/simplex-chat-nodejs/tests/bot.test.ts index b1fd9d0186..5a7faa663f 100644 --- a/packages/simplex-chat-nodejs/tests/bot.test.ts +++ b/packages/simplex-chat-nodejs/tests/bot.test.ts @@ -18,7 +18,7 @@ describe("Bot tests (use preset servers)", () => { // run bot const [chat, botUser, botAddress] = await bot.run({ profile: {displayName: "Squaring bot", fullName: ""}, - dbOpts: {dbFilePrefix: botPath, dbKey: ""}, + dbOpts: {type: "sqlite", filePrefix: botPath}, options: { addressSettings: {welcomeMessage: "If you send me a number, I will calculate its square."}, }, @@ -30,7 +30,7 @@ describe("Bot tests (use preset servers)", () => { }) assert(typeof botAddress === "object") // create user - const alice = await api.ChatApi.init(alicePath) + const alice = await api.ChatApi.init({type: "sqlite", filePrefix: alicePath}) const aliceUser = await alice.apiCreateActiveUser({displayName: "alice", fullName: ""}) await alice.startChat() // connect to bot diff --git a/plans/2026-04-06-onboarding-cards-compose.md b/plans/2026-04-06-onboarding-cards-compose.md new file mode 100644 index 0000000000..648f9e1d81 --- /dev/null +++ b/plans/2026-04-06-onboarding-cards-compose.md @@ -0,0 +1,492 @@ +# Onboarding Cards — Compose (Android/Desktop) Implementation Plan + +References the layout specification in `plans/2026-04-06-onboarding-cards-ios.md`. + +## Scope + +Same as iOS: Screens 1 and 2 with paging transition. Modal sheets for deeper views. No banner, no standalone onboarding variants. + +## New file + +`common/src/commonMain/kotlin/chat/simplex/common/views/newchat/OnboardingCards.kt` + +## Assets + +8 card stub SVGs needed in `assets/default/MR/images/` (same names as the real PNGs, with `.svg` extension): +- `card_let_someone_connect_to_you_alpha.svg` / `_light.svg` +- `card_connect_via_link_alpha.svg` / `_light.svg` +- `card_invite_someone_privately_alpha.svg` / `_light.svg` +- `card_create_your_public_address_alpha.svg` / `_light.svg` + +Real PNGs already generated in art repo `multiplatform/resources/MR/images/`. + +## Onboarding condition (shared by Android and Desktop) + +Placed in `ConnectOnboardingView.kt` as top-level functions, accessible from both `ChatListView.kt` and `App.kt`: + +```kotlin +@Composable +fun shouldShowOnboarding(): Boolean { + val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state } + val chats = chatModel.chats.value + return !addressCreationCardShown.value && chats.isNotEmpty() && noConversationChatsYet(chats) +} + +fun noConversationChatsYet(chats: List): Boolean = + chats.all { chat -> + when (val c = chat.chatInfo) { + is ChatInfo.Local -> true + is ChatInfo.Direct -> c.contact.chatDeleted || c.contact.isContactCard + is ChatInfo.Group -> false + is ChatInfo.ContactRequest -> true + is ChatInfo.ContactConnection -> true + is ChatInfo.InvalidJSON -> true + } + } +``` + +`shouldShowOnboarding` is `@Composable` (reads reactive state) and public — called from both `ChatListView.kt` and `App.kt`. `noConversationChatsYet` is a pure function, also public (used by auto-dismiss LaunchedEffect). + +### Auto-dismiss + +```kotlin +LaunchedEffect(chatModel.chats.value.size) { + if (!noConversationChatsYet(chatModel.chats.value)) { + appPrefs.addressCreationCardShown.set(true) + } +} +``` + +Placed in `ChatListWithLoadingScreen`. + +## Android integration + +### In `ChatListView.kt` — `ChatListWithLoadingScreen` (line 291) + +Change from: +```kotlin +private fun BoxScope.ChatListWithLoadingScreen(searchText, listState) { + if (!chatModel.desktopNoUserNoRemote) { ChatList(...) } + if (chatModel.chats.value.isEmpty() && ...) { Text("Loading/empty") } +} +``` + +To: +```kotlin +private fun BoxScope.ChatListWithLoadingScreen(searchText, listState) { + val chats = chatModel.chats.value + when { + chats.isEmpty() && !chatModel.switchingUsersAndHosts.value + && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == null -> { + Text(stringResource(MR.strings.loading_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) + } + shouldShowOnboarding() -> { + if (appPlatform.isAndroid) { + ConnectOnboardingView() + } + // Desktop: empty — overlay in DesktopScreen handles it + } + !chatModel.desktopNoUserNoRemote -> { + ChatList(searchText = searchText, listState) + } + } + // Auto-dismiss + LaunchedEffect(chats.size) { + if (chats.isNotEmpty() && !noConversationChatsYet(chats)) { + appPrefs.addressCreationCardShown.set(true) + } + } +} +``` + +Toolbar is a sibling in the parent `Box` (lines 150-174), stays visible. + +## Desktop integration + +### Architecture + +The overlay is the PRIMARY UI surface during onboarding. ALL interaction happens inside it — card taps, toolbar button modals, everything. `ModalManager.start` renders INTO the overlay instead of into the start panel. + +### Overlay structure in `DesktopScreen` (App.kt) + +Two visual layers in the overlay, both full-width: + +1. **Background layer:** covers center+end area only (padded left by start panel width). Opaque `MaterialTheme.colors.background`. Hides center panel content ("No selected chat") while leaving start panel fully visible underneath. + +2. **Content layer:** full window width, no background. Cards render here, centered in the full window. Clicks outside cards fall through to the start panel below. + +Both layers have top/bottom padding for toolbar height (`AppBarHeight * fontSizeSqrtMultiplier`). + +```kotlin +if (shouldShowOnboarding()) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val toolbarPadding = AppBarHeight * fontSizeSqrtMultiplier + val topPad = if (!oneHandUI.value) toolbarPadding else 0.dp + val bottomPad = if (oneHandUI.value) toolbarPadding else 0.dp + + // Background — center+end only + Box( + Modifier + .fillMaxSize() + .padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier, top = topPad, bottom = bottomPad) + .background(MaterialTheme.colors.background) + ) + + // Content — full width, cards centered + Box( + Modifier + .fillMaxSize() + .padding(top = topPad, bottom = bottomPad), + contentAlignment = Alignment.Center + ) { + ConnectOnboardingView() + } +} +``` + +Z-order: above panels and vertical divider, below `ModalManager.fullscreen`. + +### Start panel modal redirection + +During onboarding, `ModalManager.start.showInView()` renders INSIDE the overlay instead of in the start panel Box. + +In `DesktopScreen`: +```kotlin +// Start panel modals — normal location +Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) { + if (!shouldShowOnboarding()) { + ModalManager.start.showInView() + } + SwitchingUsersView() +} +``` + +Inside `ConnectOnboardingView`, on desktop: +- Watch `ModalManager.start.hasModalsOpen` +- When a start modal opens (from toolbar + button, avatar, or card tap): + 1. Cards shift RIGHT and fade to ~30% opacity (animated) + 2. `ModalManager.start.showInView()` renders on the LEFT side of the overlay with left-to-right slide animation + 3. This is the FIRST modal opening — it slides left-to-right + 4. Subsequent modals within the start modal stack open right-to-left as usual (standard `ModalManager` behavior inside the rendered area) +- When all start modals close: reverse animation — modal area slides left, cards restore position and opacity +- Clicking a faded card triggers `ModalManager.start.closeModals()` to dismiss and restore cards. This requires swapping card `onClick` handlers when `startModalsOpen` is true — each card's onClick becomes `{ ModalManager.start.closeModals() }` instead of its normal action. + +```kotlin +// Inside ConnectOnboardingView, desktop only: +val startModalsOpen = ModalManager.start.hasModalsOpen +val cardOffset by animateFloatAsState(if (startModalsOpen) 0.3f else 0f) +val cardAlpha by animateFloatAsState(if (startModalsOpen) 0.3f else 1f) +val modalSlide by animateFloatAsState(if (startModalsOpen) 0f else -1f) + +Box(Modifier.fillMaxSize()) { + // Modal area — slides from left + if (appPlatform.isDesktop) { + Box( + Modifier + .fillMaxHeight() + .widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) + .graphicsLayer { translationX = modalSlide * size.width } + ) { + ModalManager.start.showInView() + } + } + + // Cards — shift right and fade when modal open + Box( + Modifier + .fillMaxSize() + .graphicsLayer { + if (appPlatform.isDesktop) { + translationX = cardOffset * size.width + alpha = cardAlpha + } + } + ) { + HorizontalPager(...) { /* pages */ } + } +} +``` + +Card taps use `ModalManager.start.showModalCloseable` on ALL platforms — same code. On Android, the modal renders in the normal start panel modal area. On desktop during onboarding, the modal renders inside the overlay via the redirected `showInView()`. + +### Card tap actions — same on all platforms + +```kotlin +val openConnectViaLink = { + ModalManager.start.showModalCloseable { close -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, ..., close = close) + } +} +``` + +No platform branching needed. `ModalManager.start` handles the modal lifecycle. Only the rendering location changes. + +### Suppress "No selected chat" in `CenterPartOfScreen` (line 373) + +```kotlin +null -> { + if (!shouldShowOnboarding() && !rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) { + Box(...) { Text(stringResource(...)) } + } else if (!shouldShowOnboarding()) { + ModalManager.center.showInView() + } +} +``` + +When onboarding active: center panel shows nothing (overlay covers it visually). + +### Desktop HorizontalPager: tap only + +`userScrollEnabled = !appPlatform.isDesktop` — disables mouse swipe on desktop. + +## Revision 2 — Bug fixes from initial implementation + +### Fix 1: `fillMaxSize()` overrides `widthIn(max:)` + +In both page composables, the Column has `Modifier.fillMaxSize().widthIn(max = 500.dp)`. `fillMaxSize()` sets width to maximum, overriding the `widthIn` constraint. + +Fix: `Modifier.fillMaxHeight().widthIn(max = 500.dp)` — only fill height, let widthIn cap the width. + +### Fix 2: Modifier order — background before padding + +In the overlay Box: `.fillMaxSize().background(color).padding(...)` paints background over toolbar area. + +Fix: `.fillMaxSize().padding(...).background(color)` — padding first, background only fills content area. + +Superseded by the two-layer approach above — background layer is separate from content layer. + +### Fix 3: "You have no chats" text dropped + +The `when` block in `ChatListWithLoadingScreen` replaced two independent `if` blocks with mutually exclusive branches, dropping the "You have no chats" case. + +Fix: revert to the original `if` block structure, adding onboarding as the first check: +```kotlin +private fun BoxScope.ChatListWithLoadingScreen(searchText, listState) { + if (shouldShowOnboarding()) { + if (appPlatform.isAndroid) { + ConnectOnboardingView() + } + } else { + if (!chatModel.desktopNoUserNoRemote) { + ChatList(searchText = searchText, listState) + } + if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { + Text(stringResource( + if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats + ), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) + } + } + // Auto-dismiss + LaunchedEffect(chatModel.chats.value.size) { ... } +} +``` + +This preserves the original loading/empty behavior exactly. The onboarding branch is checked first — when active, it replaces everything. When inactive, original code runs unchanged. + +## ConnectOnboardingView composable + +### Structure + +```kotlin +@Composable +fun ConnectOnboardingView() { + val pagerState = rememberPagerState(initialPage = 0) { 2 } + val scope = rememberCoroutineScope() + + HorizontalPager(state = pagerState, userScrollEnabled = true) { page -> + when (page) { + 0 -> TalkToSomeonePage( + onLetSomeoneConnect = { scope.launch { pagerState.animateScrollToPage(1) } }, + onConnectViaLink = { ModalManager.start.showModalCloseable { close -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = close) + }} + ) + 1 -> ConnectWithSomeonePage( + onBack = { scope.launch { pagerState.animateScrollToPage(0) } }, + onInviteSomeone = { ModalManager.start.showModalCloseable { close -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) + }}, + onCreateAddress = { ModalManager.start.showModalCloseable { close -> + UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = close) + }} + ) + } + } +} +``` + +### Page layout + +Each page uses `BoxWithConstraints` to compute card dimensions: + +```kotlin +@Composable +private fun TalkToSomeonePage(onLetSomeoneConnect: () -> Unit, onConnectViaLink: () -> Unit) { + BoxWithConstraints(Modifier.fillMaxSize()) { + val isLandscape = maxWidth > maxHeight + val padding = 16.dp + val spacing = 16.dp + val cardWidth = if (isLandscape) (maxWidth - padding * 2 - spacing) / 2 else maxWidth - padding * 2 + val maxCardHeight = cardWidth * 0.75f + + Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) { + pageHeader("Talk to someone", showBack = false, isLandscape = isLandscape) + Spacer(Modifier.weight(1f).defaultMinSize(minHeight = 16.dp)) + cardPair(isLandscape, padding, spacing, maxCardHeight) { + // card1 and card2 + } + Spacer(Modifier.weight(1f).defaultMinSize(minHeight = 16.dp)) + } + } +} +``` + +### pageHeader composable + +Shared by both pages. No duplication: + +```kotlin +@Composable +private fun pageHeader(title: String, showBack: Boolean, isLandscape: Boolean, onBack: (() -> Unit)? = null) { + val titleView = @Composable { + Text( + stringResource(title), + style = MaterialTheme.typography.h1, // largeTitle equivalent + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + if (isLandscape) { + Box(Modifier.fillMaxWidth().padding(horizontal = 16.dp)) { + if (showBack && onBack != null) { + backButton(onBack, Modifier.align(Alignment.CenterStart)) + } + titleView() + } + } else { + Column(Modifier.fillMaxWidth().padding(horizontal = 16.dp)) { + if (showBack && onBack != null) { + backButton(onBack, Modifier.align(Alignment.Start)) + } else { + Spacer(Modifier.height(AppBarHeight)) + } + titleView() + } + } +} +``` + +Back button spacer uses `AppBarHeight` (56.dp) to match the platform's back button area, not iOS's 44pt. + +### cardPair composable + +Shared layout helper, no card duplication: + +```kotlin +@Composable +private fun cardPair( + isLandscape: Boolean, + padding: Dp, + spacing: Dp, + maxCardHeight: Dp, + card1: @Composable () -> Unit, + card2: @Composable () -> Unit +) { + if (isLandscape) { + Row(Modifier.padding(horizontal = padding), horizontalArrangement = Arrangement.spacedBy(spacing)) { + Box(Modifier.weight(1f).heightIn(max = maxCardHeight)) { card1() } + Box(Modifier.weight(1f).heightIn(max = maxCardHeight)) { card2() } + } + } else { + Column(Modifier.padding(horizontal = padding), verticalArrangement = Arrangement.spacedBy(spacing)) { + Box(Modifier.fillMaxWidth().heightIn(max = maxCardHeight)) { card1() } + Box(Modifier.fillMaxWidth().heightIn(max = maxCardHeight)) { card2() } + } + } +} +``` + +### OnboardingCardView composable + +```kotlin +@Composable +fun OnboardingCardView( + imageName: ImageResource, + imageNameLight: ImageResource, + icon: ImageResource, + title: String, + subtitle: String? = null, + labelHeightRatio: Float, + onClick: () -> Unit +) +``` + +Key Compose details (from layout-compose.md checklist): +- **Image:** `contentScale = ContentScale.Fit`, `Modifier.fillMaxSize()` — scaled AND centered ✓ +- **Gradient:** `Brush.linearGradient(colorStops, start, end)` with pixel Offsets computed from image area measured size via `Modifier.onSizeChanged` or `BoxWithConstraints` +- **Gradient math:** identical to iOS — same function ported to Kotlin, same angle/scale/aspect-ratio correction +- **Corner radius:** `RoundedCornerShape(24.dp)` with `Modifier.clip()` +- **Dark/light:** `if (isInDarkTheme()) imageNameLight else imageName` for image, gradient stops selected by theme +- **Conditional assets:** `if (BuildConfigCommon.SIMPLEX_ASSETS) { Image(...) }` +- **Clickable:** `Modifier.clip(RoundedCornerShape(24.dp)).clickable(onClick = onClick)` — clip first so ripple is bounded + +#### Label stripe background + +Use the same pattern as the toolbar (from DefaultTopAppBar.kt line 43-65): +```kotlin +MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) + .copy(alpha = appPrefs.inAppBarsAlpha.get()) +``` + +This exactly matches the toolbar appearance, including the user's bar transparency preference. + +#### Gradient in Compose + +```kotlin +// Compute inside BoxWithConstraints or onSizeChanged callback +val imageAreaSize = Size(width, imageHeight) +val (startUnit, endUnit) = gradientPoints( + aspectRatio = imageAreaSize.height / imageAreaSize.width, + scale = if (isInDarkTheme()) 1.5f else 1.2f +) +val brush = Brush.linearGradient( + colorStops = if (isInDarkTheme()) darkStops else lightStops, + start = Offset(startUnit.x * imageAreaSize.width, startUnit.y * imageAreaSize.height), + end = Offset(endUnit.x * imageAreaSize.width, endUnit.y * imageAreaSize.height) +) +``` + +### Card icons (Moko resource names) + +Screen 1: +- "Let someone connect to you" — `MR.images.ic_add_link` +- "Connect via link or QR code" — `MR.images.ic_qr_code` + +Screen 2: +- "Invite someone privately" — `MR.images.ic_add_link` +- "Create your public address" — `MR.images.ic_qr_code` + +### Strings + +8 new entries in `strings.xml` (`MR/base/strings.xml`): +```xml +Talk to someone +Let someone connect to you +Connect via link or QR code +Create your link +Invite someone privately +A link for one person to connect +Create your public address +For anyone to reach you +``` + +## Files changed + +- `ChatListView.kt` — add `shouldShowOnboarding`, `noConversationChatsYet`, modify `ChatListWithLoadingScreen`, add auto-dismiss `LaunchedEffect` +- `App.kt` — add desktop overlay in `DesktopScreen`, suppress "No selected chat" in `CenterPartOfScreen` +- `MR/base/strings.xml` — 8 new strings +- **New:** `OnboardingCards.kt` — `ConnectOnboardingView`, `OnboardingCardView`, `TalkToSomeonePage`, `ConnectWithSomeonePage`, `pageHeader`, `cardPair`, `shouldShowOnboarding`, `noConversationChatsYet`, gradient math +- **New:** 8 stub SVGs in `assets/default/MR/images/` diff --git a/plans/2026-04-06-onboarding-cards-ios.md b/plans/2026-04-06-onboarding-cards-ios.md new file mode 100644 index 0000000000..d9c310a93e --- /dev/null +++ b/plans/2026-04-06-onboarding-cards-ios.md @@ -0,0 +1,501 @@ +# Onboarding Cards — Layout Specification & iOS Implementation Plan + +## Layout Specification (cross-platform) + +This section is the authoritative reference for implementing on any platform. + +### Overall structure + +Two screens, each with a title and two tappable cards. Screens are connected by a horizontal paging transition (swipe or tap). Screen 1 has no back button; Screen 2 has a back button. Deeper views (1-time link, connect via link, SimpleX address) open as modal sheets from card taps, NOT as navigation pushes. + +The chat list toolbar (top or bottom depending on platform/settings) remains visible on both screens — the onboarding content occupies only the chat list content area. + +### Page header + +Each page has a header area containing: +- **Back button area:** fixed height 44pt. Screen 1: empty space. Screen 2: "< Back" button left-aligned. +- **Title:** centered, largeTitle font, bold, single line, shrinks to 75% minimum scale factor. +- Screen 1 title: "Talk to someone" +- Screen 2 title: "Create your link" + +**Portrait:** back button area and title are two separate rows (VStack). +**Landscape:** back button and title share one row (ZStack — back button leading, title centered). No separate back button row — saves vertical space. + +Padding: 16pt horizontal on the header container. Back button has no padding of its own. + +### Card layout + +**Portrait:** two cards stacked vertically (VStack, spacing 16pt). +**Landscape:** two cards side-by-side (HStack, spacing 16pt). + +Card horizontal padding: 16pt each side. + +Cards are vertically centered in the remaining space below the header. Equal space above and below the card group (Spacer with minLength 16pt on both sides). + +### Card max height + +Max total card height = card width × 0.75. + +In portrait: card width = screen width − 32pt (16pt padding each side). +In landscape: card width = (screen width − 32pt − 16pt spacing) / 2. + +Card height can be less than max on small screens. Height never exceeds max. + +### Card component + +Each card is a rounded rectangle (corner radius 24pt) containing: + +1. **Image area** (top) — gradient background + alpha-channel illustration overlay +2. **Label stripe** (bottom) — toolbar material background, fixed proportional height + +#### Image area + +- Gradient fills only the image area, NOT the label stripe +- Illustration: `.resizable().scaledToFit()`, fills available space, clipped to image area + +#### Gradient + +Stops (light mode): +- `#d2e8ff` (rgb 0.824, 0.910, 1.0) at 0% +- `#cce9ff` (rgb 0.800, 0.914, 1.0) at 50% +- `#dfffff` (rgb 0.875, 1.0, 1.0) at 90% +- `#fffcea` (rgb 1.0, 0.988, 0.918) at 100% + +Stops (dark mode): +- `#040a24` (rgb 0.016, 0.039, 0.141) at 40% +- `#3854ab` (rgb 0.220, 0.329, 0.671) at 72% +- `#a8edf3` (rgb 0.659, 0.929, 0.953) at 90% +- `#fff6e0` (rgb 1.0, 0.965, 0.878) at 100% + +Angle: 80° counter-clockwise from horizontal (almost vertical, slight rightward lean at top). + +Gradient scale (center-anchored): 1.2× in light mode, 1.5× in dark mode. This pushes start/end points further from center, reducing colored corner area. + +**Gradient endpoint calculation** (accounts for variable card aspect ratio): + +The gradient must maintain a constant 80° visual angle regardless of card proportions. Given the IMAGE AREA aspect ratio `r = imageHeight / width`: + +``` +θ = 80° (in radians: 80 × π / 180) +dx = cos(θ) +dy = −sin(θ) / r + +Project four corners (±0.5, ±0.5) onto direction (dx, dy): + projections = [−0.5·dx + (−0.5)·dy, 0.5·dx + (−0.5)·dy, −0.5·dx + 0.5·dy, 0.5·dx + 0.5·dy] + tMin = min(projections), tMax = max(projections) + dLenSq = dx² + dy² + +Base endpoints: + startX = 0.5 + tMin·dx/dLenSq, startY = 0.5 + tMin·dy/dLenSq + endX = 0.5 + tMax·dx/dLenSq, endY = 0.5 + tMax·dy/dLenSq + +Apply scale S (1.2 or 1.5) center-anchored: + finalStart = (0.5 + (startX−0.5)·S, 0.5 + (startY−0.5)·S) + finalEnd = (0.5 + (endX−0.5)·S, 0.5 + (endY−0.5)·S) +``` + +Important: aspect ratio uses IMAGE AREA height (card height minus label stripe), not total card height. + +#### Label stripe + +Height relative to card width: +- Single-line labels (Screen 1): 0.132 × card width +- Two-line labels (Screen 2): 0.195 × card width + +Background: platform toolbar material (matches the app toolbar appearance). On iOS: `Material` from `ToolbarMaterial` user setting. On Android: equivalent translucent material. + +Content layout: centered horizontally. +- Icon: 24pt, theme primary/accent color +- Title: body font (17pt), medium weight, theme foreground color, single line, shrinks to 75% +- Subtitle (Screen 2 only): footnote (13pt), theme foreground at 70% opacity + +Label stripe sits below the image area — gradient does NOT extend under it. + +### Card images + +8 alpha-channel PNGs (4 illustrations × light/dark variants). + +Screen 1: +- `card-let-someone-connect-to-you-alpha` / `-light` +- `card-connect-via-link-alpha` / `-light` + +Screen 2: +- `card-invite-someone-privately-alpha` / `-light` +- `card-create-your-public-address-alpha` / `-light` + +Light/dark selection: use base name on light backgrounds, `-light` suffix on dark backgrounds. + +Gated behind build flag (`#if SIMPLEX_ASSETS` on iOS, `BuildConfigCommon.SIMPLEX_ASSETS` on Android). Without assets: gradient-only cards with label stripe, still functional. + +### Card icons (SF Symbols / Material equivalents) + +Screen 1: +- "Let someone connect to you" — `link.badge.plus` +- "Connect via link or QR code" — `qrcode.viewfinder` + +Screen 2: +- "Invite someone privately" — `link.badge.plus` +- "Create your public address" — `qrcode` + +### Card actions + +Screen 1: +- Left card ("Let someone connect to you") → paging transition to Screen 2 +- Right card ("Connect via link or QR code") → modal sheet with ConnectView + +Screen 2: +- Left card ("Invite someone privately") → modal sheet with InviteView (1-time link) +- Right card ("Create your public address") → modal sheet with UserAddressView (auto-create) + +### Onboarding visibility + +Controlled by existing user default `addressCreationCardShown` (key: `"AddressCreationCardShown"`). + +Show onboarding when: +- `addressCreationCardShown == false` +- Chat list is not empty (chats have loaded) +- All chats are "ignorable" (note folders, deleted contacts, contact cards, pending connections/requests, invalid JSON) +- Any group = real conversation → onboarding hidden + +Auto-dismiss: when first real conversation appears, set `addressCreationCardShown = true` permanently. Observed via chat list count changes. + +### Strings (8) + +- "Talk to someone" +- "Let someone connect to you" +- "Connect via link or QR code" +- "Create your link" +- "Invite someone privately" +- "A link for one person to connect" +- "Create your public address" +- "For anyone to reach you" + +--- + +## Scope + +Screens 1 and 2 only — two card selection screens with slide navigation between them. No standalone onboarding variants of existing views. No banner. Those are separate future work. + +## New file + +`Shared/Views/NewChat/OnboardingCards.swift` — all new code in one file. + +## What it contains + +### `OnboardingCardView` — reusable card component + +```swift +struct OnboardingCardView: View { + @Environment(\.colorScheme) var colorScheme + let imageName: String // base asset name (without -light suffix) + let icon: String // SF Symbol name + let title: LocalizedStringKey + let subtitle: LocalizedStringKey? // nil for screen 1 cards + let action: () -> Void +} +``` + +Image selection follows the project convention: +- `colorScheme == .light` → `imageName` (base name, dark-colored image for light backgrounds) +- `colorScheme == .dark` → `"\(imageName)-light"` (light-colored image for dark backgrounds) + +Note: this only works when the base name does NOT already contain `-light`. The card image base names are like `card-let-someone-connect-to-you-alpha` — the `-alpha` suffix distinguishes them, and appending `-light` gives `card-let-someone-connect-to-you-alpha-light`. Correct. + +Structure (inside → out): +1. `Button(action:)` wrapping the entire card for tap handling, with `.buttonStyle(.plain)` to prevent default blue tint +2. Clipped to `RoundedRectangle(cornerRadius: 18)` +3. Inside, `ZStack(alignment: .bottom)`: + - `LinearGradient` filling the card shape + - `VStack(spacing: 0)`: + - `#if SIMPLEX_ASSETS` block: `Image` with `.resizable().scaledToFit().frame(maxWidth: .infinity, maxHeight: .infinity)` — takes all space above label. Image uses `.clipped()` to prevent overflow into label area. + - `#else` block: `Spacer()` — gradient-only card, label still functional. + - Label area with fixed height: `HStack(spacing: 8)` with `Image(systemName: icon)` (20pt) + `VStack(alignment: .leading, spacing: 2)` containing title + optional subtitle. Padded `(.horizontal, 16)` and `(.vertical, 12)`. + +Gradient stops (using `Color(red:green:blue:)` with values 0-1, no hex extension exists in the project): +- Light: + - `Color(red: 0.824, green: 0.910, blue: 1.0)` at 0.0 (#d2e8ff) + - `Color(red: 0.800, green: 0.914, blue: 1.0)` at 0.5 (#cce9ff) + - `Color(red: 0.875, green: 1.0, blue: 1.0)` at 0.9 (#dfffff) + - `Color(red: 1.0, green: 0.988, blue: 0.918)` at 1.0 (#fffcea) +- Dark: + - `Color(red: 0.016, green: 0.039, blue: 0.141)` at 0.4 (#040a24) + - `Color(red: 0.220, green: 0.329, blue: 0.671)` at 0.72 (#3854ab) + - `Color(red: 0.659, green: 0.929, blue: 0.953)` at 0.9 (#a8edf3) + - `Color(red: 1.0, green: 0.965, blue: 0.878)` at 1.0 (#fff6e0) +- Angle: 80° from vertical = 10° from horizontal. `LinearGradient(stops:..., startPoint: .init(x: 0.0, y: 0.6), endPoint: .init(x: 1.0, y: 0.4))`. Must verify visually — the exact start/end points for 80° depend on the view's aspect ratio. May need adjustment. + +Define the gradient stops as static properties on `OnboardingCardView` to avoid recomputing them on every recomposition. + +Label text styles: +- Title: `.body` weight `.semibold`, color `Color.white` in dark mode, `Color.primary` in light mode (from design: dark text on light gradient, light text on dark gradient). +- Subtitle: `.footnote`, color `.secondary` (adapts to theme). +- Icon: same color as title. + +### `TalkToSomeoneView` — Screen 1 + +```swift +struct TalkToSomeoneView: View { + @EnvironmentObject var theme: AppTheme + @State private var showConnectWithSomeone = false + @State private var showConnectViaLink = false +``` + +Body — NOT scrollable, fills available space: + +```swift +var body: some View { + VStack(spacing: 16) { + Text("Talk to someone") + .font(.largeTitle) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + + OnboardingCardView( + imageName: "card-let-someone-connect-to-you-alpha", + icon: "link", + title: "Let someone connect to you", + subtitle: nil, + action: { showConnectWithSomeone = true } + ) + .frame(maxHeight: .infinity) + .padding(.horizontal, 16) + + OnboardingCardView( + imageName: "card-connect-via-link-alpha", + icon: "qrcode", + title: "Connect via link or QR code", + subtitle: nil, + action: { showConnectViaLink = true } + ) + .frame(maxHeight: .infinity) + .padding(.horizontal, 16) + } + .padding(.vertical, 16) + .background( + NavigationLink(isActive: $showConnectWithSomeone) { + ConnectWithSomeoneView() + } label: { EmptyView() } + ) + .background( + NavigationLink(isActive: $showConnectViaLink) { + NewChatView(selection: .connect, showQRCodeScanner: true) + .navigationBarTitleDisplayMode(.inline) + } label: { EmptyView() } + ) +} +``` + +Key layout decisions: +- `.frame(maxHeight: .infinity)` on each card makes them share remaining vertical space equally after the title takes its natural height. +- `.padding(.vertical, 16)` on the VStack adds 16pt above the title and 16pt below the second card (VStack `spacing` only applies between children, not before first or after last). +- Hidden `NavigationLink(isActive:)` in `.background()` — drives navigation without affecting layout. This is the deprecated iOS 15 API but it works on iOS 16+ inside `NavigationStack` and is used throughout the existing codebase (e.g., `NewChatMenuButton.swift` lines 100-110). + +**`oneHandUI` inversion handling:** `TalkToSomeoneView` replaces `chatList` content. `chatListView` applies `.scaleEffect(x: 1, y: oneHandUI ? -1 : 1)` to the root page. The onboarding view gets inverted. It must counter-invert with `.scaleEffect(x: 1, y: oneHandUI ? -1 : 1)`. This is applied in `ChatListView.chatList`, NOT inside `TalkToSomeoneView` — the caller is responsible. When NavigationLink pushes Screen 2 or further views, those are new navigation pages outside the root page's scale effect, so they render normally. + +### `ConnectWithSomeoneView` — Screen 2 + +```swift +struct ConnectWithSomeoneView: View { + @EnvironmentObject var theme: AppTheme + @State private var showInviteSomeone = false + @State private var showCreateAddress = false +``` + +Same VStack layout as Screen 1, with these differences: +- Title: "Create your link" +- Card 1: imageName `"card-invite-someone-privately-alpha"`, icon `"link"`, title "Invite someone privately", subtitle "A link for one person to connect" → sets `showInviteSomeone = true` +- Card 2: imageName `"card-create-your-public-address-alpha"`, icon `"qrcode"`, title "Create your public address", subtitle "For anyone to reach you" → sets `showCreateAddress = true` + +Navigation destinations (existing views, unmodified — onboarding variants are future work): +- `showInviteSomeone` → `NewChatView(selection: .invite)` — tabbed view, 1-time link tab pre-selected. Has tabs (not ideal) but functional. +- `showCreateAddress` → `UserAddressView(shareViaProfile: false, autoCreate: true)` — auto-creates address on appear. + +Both wrapped with `.navigationBarTitleDisplayMode(.inline)`. + +Navigation bar back button shows automatically (pushed via NavigationLink within the stack). + +## Integration into ChatListView + +### In `chatList` property (line 351 of ChatListView.swift) + +Current code: +```swift +private var chatList: some View { + let cs = filteredChats() + return ZStack { + ScrollViewReader { scrollProxy in + List { ... } + } + } +} +``` + +Changed to: +```swift +@ViewBuilder +private var chatList: some View { + if shouldShowOnboarding { + TalkToSomeoneView() + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + } else { + let cs = filteredChats() + ZStack { + ScrollViewReader { scrollProxy in + List { ... } + } + } + } +} +``` + +Requires `@ViewBuilder` because `if/else` returns different view types. + +**`oneHandUI` inversion:** The `.scaleEffect(y: -1)` is applied by `chatListView` to the root page of the navigation stack. `TalkToSomeoneView` counter-inverts at the call site. When `NavigationLink` pushes Screen 2 or further, those are new navigation pages NOT affected by the root page's `.scaleEffect`. Only the root content needs the flip. + +### `shouldShowOnboarding` and `noConversationChatsYet` + +```swift +private var shouldShowOnboarding: Bool { + !addressCreationCardShown && noConversationChatsYet +} + +private var noConversationChatsYet: Bool { + chatModel.chats.allSatisfy { chat in + switch chat.chatInfo { + case .local: return true + case let .direct(contact): return contact.chatDeleted || contact.isContactCard + case let .group(groupInfo, _): return groupInfo.chatDeleted + case let .contactRequest(req): return req.chatDeleted + case let .contactConnection(conn): return conn.chatDeleted + case .invalidJSON: return true + } + } +} +``` + +Both are computed properties on `ChatListView`. `noConversationChatsYet` reads `chatModel.chats` which is `@Published` on `ChatModel` (`@EnvironmentObject`). SwiftUI re-evaluates the body when it changes, so `shouldShowOnboarding` is reactive. + +Note: `chatModel.chats` may be empty during initial load (before `APIGetChats` completes). `allSatisfy` on an empty array returns `true`. Combined with `!addressCreationCardShown`, this means the onboarding flashes briefly on app launch for users who have conversations but `chats` hasn't loaded yet. Mitigation: also check `chatModel.chats.isEmpty` and show a loading indicator instead: + +```swift +private var shouldShowOnboarding: Bool { + !addressCreationCardShown && !chatModel.chats.isEmpty && noConversationChatsYet +} +``` + +When `chats` is empty (loading), neither onboarding nor chat list shows — the existing loading state (if any) handles it. + +### Auto-dismiss + +`addressCreationCardShown` must be set to `true` when the first real conversation appears, so the onboarding never returns. + +```swift +.onChange(of: chatModel.chats.count) { _ in + if !noConversationChatsYet && !addressCreationCardShown { + addressCreationCardShown = true + } +} +``` + +Placed on `chatList` view. Observes `.count` as a proxy for chat list changes. When count changes and `noConversationChatsYet` is false, the user default is set permanently. This covers: receiving a contact request, establishing a connection, creating a group, etc. + +Edge case: chat count can change without affecting `noConversationChatsYet` (e.g., adding a second note folder). The check `!noConversationChatsYet` prevents unnecessary writes — only sets the default when there's actually a real conversation. + +## User default + +Existing `@AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false` at ChatListView line 165. Constant defined in `SettingsView.swift` line 55 as `let DEFAULT_ADDRESS_CREATION_CARD_SHOWN = "addressCreationCardShown"`. Also referenced in `AddressCreationCard.swift` line 17 and in `SettingsView.swift` defaults reset (line 114, 144). + +No new user default needed. + +## String localization + +8 new strings for `Localizable.strings` (en). Use `NSLocalizedString` or `LocalizedStringKey` inline — project uses both patterns. + +- "Talk to someone" +- "Let someone connect to you" +- "Connect via link or QR code" +- "Create your link" +- "Invite someone privately" +- "A link for one person to connect" +- "Create your public address" +- "For anyone to reach you" + +## Assets + +8 card images in art repo (4 base + 4 light variants). Run `resize.sh` then `copy-assets.sh` to populate `SimpleXAssets.xcassets`. Gated with `#if SIMPLEX_ASSETS`. Without assets: gradient-only cards with labels, still tappable and functional. + +Image base names for the `imageName` parameter: +- Screen 1: `"card-let-someone-connect-to-you-alpha"`, `"card-connect-via-link-alpha"` +- Screen 2: `"card-invite-someone-privately-alpha"`, `"card-create-your-public-address-alpha"` + +The `-light` suffix is appended automatically by `OnboardingCardView` when `colorScheme == .dark`. + +## Files changed + +- `Shared/Views/ChatList/ChatListView.swift` — add `shouldShowOnboarding`, `noConversationChatsYet`, add `@ViewBuilder` to `chatList`, branch to `TalkToSomeoneView`, add `.onChange` for auto-dismiss +- **New:** `Shared/Views/NewChat/OnboardingCards.swift` — `OnboardingCardView`, `TalkToSomeoneView`, `ConnectWithSomeoneView` + +No modifications to NewChatView, UserAddressView, or ConnectView in this phase. + +## Revision 1 — corrections from design review + +### Navigation scope (critical) +Both screens must keep the bottom/top toolbar visible. The onboarding NavigationView is SCOPED to just the card area — it does NOT replace the full chatListView. In `chatList`, wrap `TalkToSomeoneView()` in its own `NavigationView { }.navigationViewStyle(.stack)`. The toolbar from `chatListView.withToolbar()` stays outside and visible on both screens. + +Screen 1 → Screen 2: real NavigationLink push within the scoped NavigationView. +Screen 2 → deeper views: also NavigationLink pushes within same scoped NavigationView. + +### Screen 1 — reserve nav bar space +Screen 2 has a back button (navigation bar). Screen 1 must reserve the same height to prevent content shift on slide. Set `.navigationTitle("")` with `.navigationBarTitleDisplayMode(.inline)` on Screen 1's root — shows an empty inline nav bar matching Screen 2's bar height. + +### Gradient direction fix +Current gradient is nearly horizontal — wrong. Correct angle is 80° CCW from horizontal (almost vertical, slight rightward lean). + +Formula for full-coverage gradient at angle θ: +``` +startPoint = (0.5 - 0.5·cos(θ), 0.5 + 0.5·sin(θ)) +endPoint = (0.5 + 0.5·cos(θ), 0.5 - 0.5·sin(θ)) +``` + +For θ = 80°: `startPoint: .init(x: 0.413, y: 0.992), endPoint: .init(x: 0.587, y: 0.008)` + +### Corner radius +Change from 18 to 24. + +### Label stripe background +The label area has a distinct semi-transparent background strip at the bottom of the card. Add to `labelRow`: +- Light mode: `Color.white.opacity(0.5)` +- Dark mode: `Color.black.opacity(0.3)` +Exact opacity values need visual tuning. + +### Card max height ratio +Cards have a max total height/width ratio of 0.75. On tall screens, cards are capped at this ratio with extra space distributed equally above and below. On short screens, cards shrink — ratio goes below 0.75, label stripe stays fixed height, only image area shrinks. + +Implementation: use `GeometryReader` to get available width, compute `maxCardHeight = cardWidth * 0.75`, apply `.frame(maxHeight: maxCardHeight)` on each card. The VStack centers vertically in the GeometryReader — equal space above and below on tall screens. + +### Title alignment +Change from `.leading` to `.center` — design shows centered titles on both screens. + +### Subtitle color in dark mode +Change `.foregroundColor(.secondary)` to `.foregroundColor(colorScheme == .dark ? .white.opacity(0.7) : .secondary)` — standard `.secondary` is too gray on the dark gradient. + +### Label stripe height proportions +The label stripe has fixed proportional heights relative to card width: +- Screen 1 (single-line labels): 0.132 × card width +- Screen 2 (two-line labels): 0.195 × card width + +These are achieved via fixed padding on the label row. The image area is the remainder of the card height. When cards shrink on short screens, only the image area shrinks — the label stripe stays at its proportional height. + +### Spacing between title and cards, between cards, and below cards +The gaps above first card and below second card should be EQUAL and LARGER than the gap between the two cards. The inter-card gap is the VStack spacing (~16pt). The outer gaps are larger — achieved by the GeometryReader centering the VStack vertically, which distributes extra space equally above and below. + +### ThemedBackground on TalkToSomeoneView +`TalkToSomeoneView` needs `.modifier(ThemedBackground())` — it replaces `chatList` content and needs its own background. Currently missing. + +### `oneHandUI` inversion on Screen 2 +The scoped `NavigationView` sits inside `chatList` which is visually inverted by `chatListView`'s `.scaleEffect(y: -1)`. This inversion applies to the NavigationView's rendered frame — ALL pages inside it (both Screen 1 and Screen 2) are inverted. `TalkToSomeoneView` counter-inverts at the call site. `ConnectWithSomeoneView` (pushed within the NavigationView) also needs counter-inversion. Pass `oneHandUI` as a binding or read from `@AppStorage(GROUP_DEFAULT_ONE_HAND_UI)` directly inside `ConnectWithSomeoneView`, and apply `.scaleEffect(x: 1, y: oneHandUI ? -1 : 1)` on its root VStack. Same for any deeper pushed views — but those are existing views not modified in this phase, so their inversion behavior needs testing. + +### Plan cleanup note +The original sections above contain outdated code snippets (wrong gradient, wrong corner radius, wrong switch cases, wrong alignment). The Revision 1 sections are authoritative. When implementing, follow Revision 1 values; treat original sections as structural context only. + diff --git a/plans/2026-04-10-relay-leaving-group.md b/plans/2026-04-10-relay-leaving-group.md new file mode 100644 index 0000000000..42aebf425b --- /dev/null +++ b/plans/2026-04-10-relay-leaving-group.md @@ -0,0 +1,234 @@ +# Plan: Relay Leaving Group (Moderation Capability) + +## Context + +SimpleX Chat channels use chat relays to forward messages from owners to subscribers. When a channel hosts prohibited content, the relay operator needs the ability to make their relay leave the group. Currently `APILeaveGroup` doesn't work correctly for relay members: the `getRecipients` helper always uses `getGroupRelayMembers` for channel groups, which returns only other relays (members with `GRRelay` role) — the owner is excluded. The relay needs to notify the owner (so it can update channel link data) and all subscribers directly (relay has connections to all of them). + +## Flow + +1. **Relay** calls `APILeaveGroup` → sends `XGrpLeave` directly to all members (owners + subscribers) → deletes all connections +2. **Owner** receives `XGrpLeave` → updates relay member status to `GSMemLeft` → updates `GroupRelay.relayStatus` to `RSInactive` → updates channel link relay list via `updatePublicGroupData` → `setGroupLinkDataAsync` → `setAgentConnShortLinkAsync` (excludes left relay from link data) → relay bar shows status +3. **Subscribers** receive `XGrpLeave` directly from relay → update relay member status to `GSMemLeft` → delete connection to relay → relay bar shows status + +## Changes + +### 1. Add `RSInactive` to `RelayStatus` + +**`src/Simplex/Chat/Types/Shared.hs`** (~L81-112) + +`GroupMemberStatus` already carries left/removed semantics (`GSMemLeft`, `GSMemRemoved`), so `RelayStatus` should not duplicate that. Add `RSInactive` as a generic terminal status meaning "no longer operational", complementing `RSActive`. Add `"inactive"` encoding in `relayStatusText` and `TextEncoding` instance. + +```haskell +data RelayStatus + = RSNew + | RSInvited + | RSAccepted + | RSActive + | RSInactive + deriving (Eq, Show) +``` + +### 2. Fix `APILeaveGroup` recipients for relay + +**`src/Simplex/Chat/Library/Commands.hs`** (~L2838-2844) + +Use nested condition inside `useRelays'` guard. When relay leaves, it sends `XGrpLeave` to all current/pending members directly (relay has connections to all of them). + +```haskell +getRecipients user gInfo@GroupInfo {membership} + | useRelays' gInfo = + if isRelay membership + then do + -- Relay leaving: notify all members directly, clean up all connections + ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo + pure (ms, filter memberCurrentOrPending ms) + else do + relays <- withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo + pure (relays, relays) + | otherwise = do + ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo + pure (ms, filter memberCurrentOrPending ms) +``` + +- `members` (first tuple) = all members → used by `deleteMembersConnections'` for connection cleanup +- `recipients` (second tuple) = all current/pending members → XGrpLeave sent directly + +Existing functions: `isRelay` (Types.hs:1063), `getGroupMembers` (Store/Groups.hs), `memberCurrentOrPending` (Types.hs:1308). + +### 3. Update `xGrpLeave` to set relay status and channel link on owner + +**`src/Simplex/Chat/Library/Subscriber.hs`** (~L3113-3124) + +After `updateMemberRecordDeleted` and before `updatePublicGroupData`, set `RSInactive` on the owner's `GroupRelay` record. On subscribers this is a no-op because they have no `GroupRelay` record (`getGroupRelayByGMId` returns `Left`, `forM_` skips). + +```haskell +xGrpLeave gInfo m msg@RcvMessage {msgSigned} brokerTs = do + deleteMemberConnection m + gInfo' <- updateMemberRecordDeleted user gInfo m GSMemLeft + -- Set relay status to inactive (owner-only; subscriber has no GroupRelay record) + when (isRelay m) $ + withStore' $ \db -> do + relay_ <- runExceptT $ getGroupRelayByGMId db (groupMemberId' m) + forM_ relay_ $ \relay -> void $ updateRelayStatus db relay RSInactive + gInfo'' <- updatePublicGroupData user gInfo' + -- ... rest unchanged +``` + +The channel link update chain on owner: `updatePublicGroupData` (Internal.hs:1317) → `setGroupLinkDataAsync` (Internal.hs:1309) → `getConnectedGroupRelays` (filters `member_status = GSMemConnected AND relay_status IN (RSAccepted, RSActive)`) → `groupLinkData` (builds `UserContactLinkData` with remaining relay links only) → `setAgentConnShortLinkAsync` (updates SMP short link). The left relay is excluded by the `member_status` filter, so its link is removed from the channel link data. + +Existing functions: `isRelay` (Types.hs:1063), `getGroupRelayByGMId` (Store/Groups.hs:1296), `updateRelayStatus` (Store/Groups.hs:1418), `groupMemberId'` (Types.hs). + +### 4. Client type updates + +**`apps/ios/SimpleXChat/ChatTypes.swift`** (~L2558-2563, L2628-2637) + +Add `case rsInactive = "inactive"` to `RelayStatus` enum and `case .rsInactive: "inactive"` to `text` property. + +**`apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt`** (~L2266-2276) + +Add `@SerialName("inactive") RsInactive` to `RelayStatus` enum and `RsInactive -> generalGetString(MR.strings.relay_status_inactive)` to `text` property. + +**`apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml`** (~L2879) + +Add `inactive`. + +**`packages/simplex-chat-client/types/typescript/src/types.ts`** (~L3608-3613) + +Add `Inactive = "inactive"` to `RelayStatus` enum. + +### 5. UI: relay bar updates + +The relay bar (above compose area) currently shows for owners when `activeCount < relays.count`, and for subscribers in steady state. After relay leaves, the bar shows but needs better indication of what happened and whether delivery is broken. + +#### 5a. Fix `relayStatusIndicator` color for `RSInactive` + +Currently `relayStatusIndicator` shows yellow for any non-active status — yellow implies "connecting/in progress", which is wrong for an inactive relay. + +**iOS** (`apps/ios/Shared/Views/NewChat/AddChannelView.swift` L431-432): +```swift +// CURRENT: +let color: Color = connFailed ? .red : (status == .rsActive ? .green : .yellow) +// NEW: +let color: Color = connFailed ? .red : (status == .rsActive ? .green : (status == .rsInactive ? .red : .yellow)) +``` + +**Kotlin** (`apps/multiplatform/.../views/newchat/AddChannelView.kt` L551): +```kotlin +// CURRENT: +val color = if (connFailed) Color.Red else if (status == RelayStatus.RsActive) Color.Green else WarningYellow +// NEW: +val color = if (connFailed) Color.Red else when (status) { + RelayStatus.RsActive -> Color.Green + RelayStatus.RsInactive -> Color.Red + else -> WarningYellow +} +``` + +#### 5b. Owner relay bar: "no active relays" message + +When `activeCount == 0`, show a warning in the relay bar that delivery is broken and adding new relays is coming. + +**iOS** (`apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift` ~L730-738): + +In `ownerChannelRelayBar`, when expanded and `activeCount == 0`, add footer text: +```swift +"Messages can't be delivered to subscribers. Adding new relay will be available in a future update." +``` + +**Kotlin** (`apps/multiplatform/.../views/chat/ComposeView.kt` ~L1647-1657): same logic. + +New string: `relay_bar_owner_no_delivery` = "Messages can't be delivered to subscribers. Adding new relay will be available in a future update." + +#### 5c. Subscriber relay bar: show disconnection in steady state + +Currently in steady state (`showProgress = false`), the subscriber relay bar header shows only "N relays" with no error indication. When relay connections are deleted (relay left), the subscriber sees no issue in collapsed view. + +**iOS** (`apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift` ~L780-792): + +When `!showProgress`, check error state: +```swift +if !showProgress { + if errorCount == total { + Text("All relays disconnected – messages can't be delivered") + } else if errorCount > 0 { + Text(String.localizedStringWithFormat("%d/%d relays connected, %d errors", connectedCount, total, errorCount)) + } else { + Text(String.localizedStringWithFormat("%d relays", total)) + } +} +``` + +**Kotlin** (`apps/multiplatform/.../views/chat/ComposeView.kt` ~L1695-1707): same logic. + +New strings: +- `relay_bar_all_disconnected` = "All relays disconnected – messages can't be delivered" +- `relay_bar_connected_with_errors_steady` = "%1$d/%2$d relays connected, %3$d errors" + +### 6. Test + +**`tests/ChatTests/Groups.hs`** + +Add `testChannelRelayLeave` test: + +1. Create channel with 2 relays (`relay1`, `relay2`) and 2 subscribers (`dan`, `eve`) via `prepareChannel2Relays` + `memberJoinChannel` +2. Verify channel works: owner sends message → subscribers receive via relay forwarding +3. `relay1` leaves: `relay1 ##> "/leave #team"` +4. Verify relay1 output: `"#team: you left the group"` +5. Verify owner output: `"#team: left the group (signed)"` +6. Verify subscribers receive `XGrpLeave` directly — check relay1 member status is `"left"` on subscribers via `checkMemberStatus` +7. Wait for async link data update +8. Verify channel still works with remaining relay: owner sends message → relay2 forwards → subscribers receive +9. `relay2` leaves: `relay2 ##> "/leave #team"` +10. Verify relay2 output and owner/subscriber leave events +11. **Verify no delivery**: owner sends message, `threadDelay`, check subscribers' last item is still the previous message (not the new one) — pattern from `testChannelSubscriberLeave` L9237 + +Register test in test list at ~L261 (after `testChannelSubscriberLeave`). + +## Files Modified + +| File | Change | +|------|--------| +| `src/Simplex/Chat/Types/Shared.hs` | Add `RSInactive` to `RelayStatus` | +| `src/Simplex/Chat/Library/Commands.hs` | Fix `getRecipients` for relay in `APILeaveGroup` | +| `src/Simplex/Chat/Library/Subscriber.hs` | Update `xGrpLeave` to set relay status | +| `apps/ios/SimpleXChat/ChatTypes.swift` | Add `rsInactive` case | +| `apps/ios/Shared/Views/NewChat/AddChannelView.swift` | Fix `relayStatusIndicator` color for inactive | +| `apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift` | Owner/subscriber relay bar messages | +| `apps/multiplatform/.../ChatModel.kt` | Add `RsInactive` case | +| `apps/multiplatform/.../AddChannelView.kt` | Fix `RelayStatusIndicator` color for inactive | +| `apps/multiplatform/.../ComposeView.kt` | Owner/subscriber relay bar messages | +| `apps/multiplatform/.../strings.xml` | Add `relay_status_inactive` + relay bar strings | +| `packages/.../types.ts` | Add `Inactive` to enum | +| `tests/ChatTests/Groups.hs` | Add `testChannelRelayLeave` test | + +## Verification + +```bash +cabal build --ghc-options=-O0 +cabal test simplex-chat-test --test-options='-m "channels"' +``` + +Manual: verify relay bar appearance on iOS simulator and Android emulator after relay leaves. + +## Adversarial Review + +**Pass 1:** +- Relay's `sendGroupMessage'` signs `XGrpLeave` (`requiresSignature XGrpLeave_ = True`; relay has `groupKeys` with `memberPrivKey`). Owner and subscribers verify signature. OK. +- `deleteMembersConnections' user members True` with `waitDelivery=True` ensures `XGrpLeave` reaches SMP queues before relay deletes connections. OK. +- Owner's channel link update: `updatePublicGroupData` → `setGroupLinkDataAsync` → `getConnectedGroupRelays` (excludes left relay by `member_status = GSMemLeft`) → `groupLinkData` (builds link with remaining relays) → `setAgentConnShortLinkAsync` (updates SMP short link). Left relay's link removed. OK. +- `muteEventInChannel` for relay member (`GRRelay < GRModerator`): muted for subscribers (no chat item), but DB is updated. Owner sees event (`GROwner >= GRModerator`). OK. +- Relay's `deleteGroupLinkIfExists` is no-op (relay doesn't own group link). OK. +- `getGroupRelayByGMId` on subscriber returns `Left` (no `GroupRelay` record) → `forM_` skips. No error. OK. +- `memberEventDeliveryScope` returns `DJSGroup {jobSpec = DJDeliveryJob {includePending = False}}` for relay member. On subscriber, creates a delivery task but subscriber has no forwarding role — delivery worker finds no eligible connections. Harmless no-op. OK. +- Owner relay bar: `activeCount` drops (RSInactive ≠ RSActive) → bar shows → `relayStatusIndicator` shows red dot with "inactive". When `activeCount == 0`: "Messages can't be delivered... Adding new relay will be available in a future update." OK. +- Subscriber relay bar: `connStatus = .deleted` → `deletedCount` increases → `errorCount` increases. When `errorCount == total` in steady state: "All relays disconnected – messages can't be delivered". OK. + +**Pass 2:** +- Race condition: owner removes relay + relay leaves simultaneously. Both paths delete connection and update member status. No data corruption — idempotent. `GroupRelay.relayStatus` ends as `RSInactive` from `xGrpLeave` or unchanged from `xGrpMemDel` (which doesn't update relay status). OK. +- `getGroupMembers` for relay may return thousands of subscribers. `deleteMembersConnections'` uses `deleteAgentConnectionsAsync'` which handles batching. `sendGroupMessage'` also handles sending to many members. OK. +- Relay's own `membership` record has no `activeConn` in members list (can't connect to self). `mapMaybe` in `deleteMembersConnections'` filters it out. OK. +- Both relays leave: after last relay leaves, owner sends message. Delivery system has no connected relays to forward through — message saved locally but not delivered. Subscribers' last chat item remains unchanged. Test verifies this. OK. +- `getGroupRelays` (used by `apiGetGroupRelays`) returns ALL GroupRelay records including RSInactive — owner UI correctly includes left relays in bar. OK. +- Subscriber `groupMembers` filter (`memberRole == .relay`) includes left members (memberRole unchanged) — subscriber UI correctly shows left relays. OK. + +**Result: 2 consecutive clean passes.** diff --git a/plans/2026-04-11-channel-invitations-directory.md b/plans/2026-04-11-channel-invitations-directory.md new file mode 100644 index 0000000000..6ae2c046db --- /dev/null +++ b/plans/2026-04-11-channel-invitations-directory.md @@ -0,0 +1,255 @@ +# Public Group Invitations & Directory Listing + +## Goal + +Enable public group (channel) subscribers to invite new subscribers by sharing a channel card in any chat where they can send messages. Channel owners can prove ownership via a signed card. This unblocks directory service support for public groups alongside regular groups. + +Sharing channels should be as simple as forwarding — share button on channel opens chat picker, sends a channel card as a regular message. Old clients show the text; new clients show a rich card with profile and join button. + +## Context + +### Current state +- Public groups have `PublicGroupProfile {groupType = GTChannel, groupLink, publicGroupId}` and `useRelays = True` +- Users join public groups via link → `APIPrepareGroup` → `APIConnectPreparedGroup` +- `MCChat` message content exists with `MsgChatLink` variants for contacts, invitations, and groups (`MCLGroup`) +- Group invitations (`XGrpInv`) carry `connRequest :: ConnReqInvitation` — public groups don't use this mechanism +- Directory bot registers groups via group invitation (owner invites bot as admin) — public groups need a different flow + +### Owner keys in public group links +- `FixedLinkData.rootKey :: PublicKeyEd25519` — genesis root key +- `UserContactData.owners :: [OwnerAuth]` — chain of authorized owner keys, each signed by root or previous owner +- Public group creator stores `GroupKeys {groupRootKey = GRKPrivate rootPrivKey, memberPrivKey}` +- `memberPrivKey`'s public key = `ownerKey` in the `OwnerAuth` entry (created via `newOwnerAuth`) +- `publicGroupId = sha256(rootPubKey)` — immutable group identity + +### DR connection shared secret +- Each direct connection has `rcAD` (Associated Data) from X3DH key exchange +- `getConnectionRatchetAdHash` returns `sha256(rcAD)` — binding for replay protection + +## Design + +### Channel cards as MCChat messages + +Channel invitations are sent as regular `XMsgNew` with `MCChat` content. No new protocol messages. + +```haskell +data MsgContent + = ... + | MCChat {text :: Text, chatLink :: MsgChatLink, ownerSig :: Maybe LinkOwnerSig} + | ... +``` + +`ownerSig` is optional. Old clients ignore it (missing field) and show `text` as a regular message. + +```haskell +data LinkOwnerSig = LinkOwnerSig + { ownerId :: Maybe OwnerId, -- Nothing = root key, Just = owner key from OwnerAuth chain + binding :: B64UrlByteString, + ownerSig :: B64UrlByteString + } +``` + +Sending is supported for channel cards only (for now). Verification is generic for all `MsgChatLink` types: +- `ownerId = Just id`: verified against matching `OwnerAuth.ownerKey` in the link's owner chain (channels) +- `ownerId = Nothing`: verified against `rootKey` from `FixedLinkData` (contacts, invitations) + +The sender proves control over the link regardless of type. + +### What is signed + +`smpEncode chatBinding <> bindingData <> smpEncode chatLink` signed with `memberPrivKey`. + +Binding depends on where the card is sent: +- **Direct chat**: `CBDirect` with `ratchetAdHash` +- **Public group**: `CBGroup` with `smpEncode (publicGroupId, memberId)` +- **Group without public identity**: signature treated as failed at verification time + +Binding is to chat, not to message (`sharedMsgId` is not included). This allows the sender to forward their own signed card within the same chat (e.g., re-sharing a channel link as a reminder) without invalidating the signature. Message-level binding would prevent this since forwarded messages get new `sharedMsgId`s. + +### Sending flow + +1. User presses "Share" on channel → API call `APIPrepareLinkOwnerSig GroupId` returns `Maybe LinkOwnerSig` +2. Opens chat picker (same as forwarding) — chats with disabled simplex links greyed out +3. Sends `XMsgNew` with `MCChat {text = displayName, chatLink = MCLGroup {connLink, groupProfile}, ownerSig}` +4. Creates regular `CISndMsgContent` chat item — no new item types, no new response types + +### Receiving flow + +Regular `XMsgNew` processing. Creates `CIRcvMsgContent (MCChat ...)`. No hidden groups, no async verification, no special events. + +UI renders channel card with profile, member count, join button. If `ownerSig` present, shows "signed by owner" indicator (unverified until join). + +### Verification at join time + +When user taps "Join" on a channel card: + +1. UI extracts `connLink` and `ownerSig` from `MCChat` message content +2. UI calls `APIConnectPlan` with the link and signature. `APIConnectPlan` extended: + ```haskell + APIConnectPlan {userId :: UserId, connectionLink :: Maybe AConnectionLink, linkOwnerSig :: Maybe LinkOwnerSig} + ``` + Parser: `/_connect plan [sig=]` +3. Inside `connectPlan`, if `linkOwnerSig` is present: + - Gets `FixedLinkData {rootKey}` and `UserContactData {owners}` from resolved link + - Finds verification key: `ownerId = Nothing` → `rootKey`, `ownerId = Just id` → matching `OwnerAuth.ownerKey` + - Verifies binding data against expected value from context + - Verifies signature +4. Each "OK" plan variant extended with verification result: + ```haskell + data LinkSigVerification = LSVVerified | LSVFailed {reason :: Text} + + ILPOk {contactSLinkData_, linkSigVerification :: Maybe LinkSigVerification} + CAPOk {contactSLinkData_, linkSigVerification :: Maybe LinkSigVerification} + GLPOk {groupSLinkInfo_, groupSLinkData_, linkSigVerification :: Maybe LinkSigVerification} + -- Nothing = not signed, Just LSVVerified = verified, Just LSVFailed = failed with reason + ``` + Reasons: "unknown owner ID", "binding data mismatch", "signature verification failed", "no group identity for verification" +5. UI shows verification result in join/connect alert for the OK plan variants +6. User confirms → `APIPrepareGroup` → `APIConnectPreparedGroup` — existing join flow, no changes + +Pasted links (no message context) pass `linkOwnerSig = Nothing` — plan shows "not signed." + +### Forwarding + +When `MCChat` is forwarded, `ownerSig` is dropped — UNLESS forwarded by sender in the same chat (re-sharing own card as reminder). Signature is bound to chat context, so forwarding in the same chat preserves validity. + +Implementation: in forwarding code, drop `ownerSig` unless `fromChatRef == toChatRef` and sender is the same user. + +### Simplex link permission + +`MCChat` IS a simplex link — if `SGFSimplexLinks` is prohibited for the sender's role, `MCChat` should be prohibited regardless of content. + +Currently `prohibitedSimplexLinks` (Internal.hs:363) only checks formatted text. Fix: also check `MsgContent` type — if it's `MCChat` and simplex links are not allowed, prohibit it. This covers both send and receive via existing `prohibitedGroupContent` calls. + +For backward compatibility, the current text-level check is sufficient since the link is included in `text`. But the `MCChat` type check is the correct long-term fix. + +### CLI view + +`MCChat` with `MCLGroup` renders as channel card with display name. If `ownerSig` present, shows "(signed)" indicator. + +## Directory bot changes + +### Registration flow + +Bot receives regular `CIRcvMsgContent (MCChat ...)` messages in direct chat from channel owners. Bot checks `ownerSig` is present. Verifies at join time via `connectPlan`. No special events needed. + +- Owner sends channel card to bot in DM (signed) +- Bot resolves link, verifies owner signature +- Bot joins channel as subscriber +- Simplified approval flow: `GRSProposed` → `GRSPendingApproval` → `GRSActive` + +### Profile monitoring + +Bot as subscriber receives `XGrpInfo` when owner updates profile. On profile change: re-resolve link, compare. Periodic re-verification. + +### Search and listing + +Search includes both groups and public groups. No separate listing category — `groupProfile.publicGroup` is the source of truth. `DETGroup` works for both in JSON listing. + +## Implementation plan (diff from master) + +### Step 1: LinkOwnerSig type + +- `LinkOwnerSig` type in Types.hs (or Protocol.hs alongside `MCChat`) +- `ownerSig :: Maybe LinkOwnerSig` field on `MCChat` +- JSON derivation with backward compat (optional field) + +### Step 2: CBDirect + +- Add `CBDirect` to `ChatBinding` in Protocol.hs (already done on master via refactoring PR) + +### Step 3: Share chat message content API + +New command that constructs the complete `MCChat` content for sharing: +```haskell +-- Controller.hs +APIShareChatMsgContent {shareChatRef :: ChatRef, toChatRef :: ChatRef} +-- returns CRChatMsgContent {user :: User, msgContent :: MsgContent} +``` + +Implementation in Commands.hs: +1. Load shared chat info from `shareChatRef` — initially only `CTGroup` with public groups supported +2. Get `PublicGroupProfile {groupLink}` and `groupProfile` from group +3. Determine if user is owner (has `GroupKeys {memberPrivKey}`) +4. If owner, compute binding based on `toChatRef`: + - `ChatRef CTDirect contactId` → `getConnectionRatchetAdHash` on contact's connection → `CBDirect` + - `ChatRef CTGroup groupId` → `smpEncode (publicGroupId, memberId)` if group has identity → `CBGroup` + - Group without identity → `Nothing` (can't sign) +5. If owner and binding available, sign `smpEncode chatBinding <> bindingData <> smpEncode chatLink` with `memberPrivKey` +6. Return `MCChat {text = displayName, chatLink = MCLGroup {connLink = groupLink, groupProfile}, ownerSig}` + +Parser: `/_share_chat ` + +UI flow: press Share on channel → chat picker → select destination → call `APIShareChatMsgContent` → get `MsgContent` → send via existing `APISendMessages` + +All business logic (ownership check, signing decision, link extraction, profile inclusion) stays in core. UI only passes two chat refs and sends the returned content. + +### Step 4: connectPlan verification + +Extend `APIConnectPlan` (Controller.hs:472): +```haskell +APIConnectPlan {userId :: UserId, connectionLink :: Maybe AConnectionLink, linkOwnerSig :: Maybe LinkOwnerSig} +``` + +Parser (Commands.hs:4945): extend to accept optional JSON `LinkOwnerSig` parameter. + +In `connectPlan` (Commands.hs), pass `linkOwnerSig` to `groupShortLinkPlan` / `groupJoinRequestPlan`. + +In `groupShortLinkPlan` (Commands.hs ~line 3944): after resolving the link via `getShortLinkConnReq`, if `linkOwnerSig` is present: +1. Extract `FixedLinkData {rootKey}` and `UserContactData {owners}` +2. If `ownerId = Nothing`: verify against `rootKey` +3. If `ownerId = Just id`: find `OwnerAuth` where `ownerId == id`, verify against `ownerKey` +4. Check binding data matches expected +5. Verify signature + +Extend `GroupLinkPlan` (Controller.hs:1025): +```haskell +GLPOk {groupSLinkInfo_, groupSLinkData_, ownerVerified :: Maybe Bool} +``` +`Nothing` = not signed, `Just True` = verified, `Just False` = failed. + +`CRConnectionPlan` response carries this through to UI — shown in plan alert. + +### Step 5: Forwarding — drop ownerSig + +In message forwarding code (Commands.hs, `APIForwardChatItems`), when forwarding `MCChat` content, set `ownerSig = Nothing`. + +Location: Commands.hs where forwarded message content is constructed — find where `MCChat` is handled in forwarding and strip the signature. + +### Step 6: Permission check + +Fix `prohibitedSimplexLinks` (Internal.hs:363) to also check `MsgContent` type — if `MCChat`, treat as simplex link. Covers both send and receive paths via existing `prohibitedGroupContent` calls. + +For backward compatibility, the link is also in `text` field, so existing text-level check catches it. The type check is the correct fix. + +### Step 7: CLI view + +In `viewChatItem` (View.hs), `MCChat` content already renders via `ttyMsgContent`. Extend to show channel card format and "(signed)" indicator when `ownerSig` is present. + +### Step 8: groupLinkData owners preservation + +Fix `groupLinkData` (Internal.hs:1330) to reconstruct `OwnerAuth` from `GroupKeys` instead of hardcoding `owners = []`. This ensures the resolved link data has the owner keys needed for verification. + +Implementation: when `GroupKeys` has `GRKPrivate rootPrivKey` and `memberPrivKey`, reconstruct `OwnerAuth` with `ownerId = unMemberId memberId`, `ownerKey = publicKey memberPrivKey`, `authOwnerSig = sign rootPrivKey (ownerId <> encodePubKey ownerKey)`. + +### Step 9: Tests + +- Share channel card in direct chat (owner signed) +- Share channel card in group (unsigned — no binding for groups without identity) +- Share channel card in channel +- Join via channel card — verify `connectPlan` shows verification result +- Non-public group share rejected +- Forwarded card has no signature +- Old client compatibility (text field shown) + +### Step 10: Directory bot + +- Handle `MCChat` with `MCLGroup` in `crDirectoryEvent_` +- Channel registration flow +- Profile monitoring + +## What stays from refactoring PR (already on master) + +- `CBDirect` in `ChatBinding` +- `HasShortLink` typeclass with `connShortLink'` +- `setShortLinkType` / `setShortLinkType_` diff --git a/plans/2026-04-16-ios-share-channel-link.md b/plans/2026-04-16-ios-share-channel-link.md new file mode 100644 index 0000000000..414016c9c4 --- /dev/null +++ b/plans/2026-04-16-ios-share-channel-link.md @@ -0,0 +1,407 @@ +# iOS — Share chat card (MCChat) + +Share a public group/channel link as a card in any chat. Backend (`APIShareChatMsgContent`, `SharePublicGroup`) exists. This plan covers: send-side UI (picker, compose, send), receive-side UI (card rendering, tap-to-connect with owner verification), and the plumbing between. + +--- + +## 0. UX flow + +1. Channel info screen (`GroupChatInfoView`): two entry points. + - **Quick-access action button** row (next to search/mute): "share" button — visible when `groupInfo.useRelays` and channel has a public link. Non-owners see it too. + - **Section button**: "Share via chat" inside the existing channel-link section (after existing system "Share link" button). +2. Tap either → **destination picker sheet** (reused from `ChatItemForwardingView` by parameterization, NOT a new view). +3. Tap a destination → sheet dismisses, navigates to the destination chat, compose shows **plaque** above input: "Sharing #channelName" (reused `ContextItemView` by parameterization, NOT a new view). +4. User may type optional text. Tap Send. +5. Backend builds MCChat: `text = \n` (link appended for old clients), signed with owner key if applicable. Sent as one message. +6. **Receive side**: card renders as a group-invitation-style tile (reused from `CIGroupInvitationView` by extracting shared component, NOT copy-pasted). Shows profile image + channel name + icon per link type + "from channel owner" if `ownerSig` present. Tap → `planAndConnect` flow with `linkOwnerSig` → alert shows owner verification result alongside standard plan info. + +--- + +## B. Implementation, file by file + +### 1. `SimpleXChat/ChatTypes.swift` — `LinkOwnerSig`, `OwnerVerification`, `MsgContent.chat` update + +**LinkOwnerSig** (new struct, near `MsgChatLink` ~line 4790): +```swift +public struct LinkOwnerSig: Codable, Equatable, Hashable { + public let ownerId: String? + public let chatBinding: String + public let ownerSig: String +} +``` + +**OwnerVerification** (new enum, near `GroupLinkPlan` in AppAPITypes.swift ~line 1379): +```swift +enum OwnerVerification: Decodable, Hashable { + case verified + case failed(reason: String) +} +``` + +**MsgContent.chat** — add `ownerSig` field: +- Case: `case chat(text: String, chatLink: MsgChatLink, ownerSig: LinkOwnerSig?)` +- CodingKeys: add `case ownerSig` +- Decoder (4719-4722): add `let ownerSig = try container.decodeIfPresent(LinkOwnerSig.self, forKey: .ownerSig)` +- Encoder (4764-4767): add `try container.encodeIfPresent(ownerSig, forKey: .ownerSig)` +- text getter (4605): `case let .chat(text, _, _)` +- `==` (4679): `case let (.chat(lt, ll, ls), .chat(rt, rl, rs)): return lt == rt && ll == rl && ls == rs` +- ComposeView.swift:1480: `case let .chat(_, chatLink, ownerSig): return .chat(text: msgText, chatLink: chatLink, ownerSig: ownerSig)` + +### 2. `Shared/Model/AppAPITypes.swift` — `SendRef`, plan types, command, response + +**SendRef** (new enum, near `ref()` helper ~line 580): +```swift +enum SendRef { + case direct(contactId: Int64) + case group(groupId: Int64, scope: GroupChatScope?, asGroup: Bool) +} + +func sendRef(_ r: SendRef) -> String { + switch r { + case let .direct(contactId): "@\(contactId)" + case let .group(groupId, scope, asGroup): + "#\(groupId)\(scopeRef(scope))\(asGroup ? "(as_group=on)" : "")" + } +} +``` + +**Plan types — add `ownerVerification`** to `.ok` cases: +- `InvitationLinkPlan.ok` (1352): `case ok(contactSLinkData_: ContactShortLinkData?, ownerVerification: OwnerVerification?)` +- `ContactAddressPlan.ok` (1359): `case ok(contactSLinkData_: ContactShortLinkData?, ownerVerification: OwnerVerification?)` +- `GroupLinkPlan.ok` (1374): `case ok(groupSLinkInfo_: GroupShortLinkInfo?, groupSLinkData_: GroupShortLinkData?, ownerVerification: OwnerVerification?)` + +**ChatCommand** — new case after `apiForwardChatItems` (line 64): +```swift +case apiShareChatMsgContent(shareChatType: ChatType, shareChatId: Int64, toSendRef: SendRef) +``` +cmdString: `"/_share chat content \(ref(shareChatType, shareChatId, scope: nil)) \(sendRef(toSendRef))"` + +**APIConnectPlan** — extend with `linkOwnerSig`: +- Current (line ~150ish): `case apiConnectPlan(userId: Int64, connLink: String?)` +- Change to: `case apiConnectPlan(userId: Int64, connLink: String?, linkOwnerSig: LinkOwnerSig?)` +- cmdString: append `linkOwnerSig.map { " sig=" + encodeJSON($0) } ?? ""` (matches Haskell parser `optional (" sig=" *> jsonP)`) + +**ChatResponse1** — new case after `newChatItems` (823): +```swift +case chatMsgContent(user: UserRef, msgContent: MsgContent) +``` +Plus `responseType` + `details` entries. + +### 3. `Shared/Model/SimpleXAPI.swift` — wrappers + +**apiShareChatMsgContent** (near apiForwardChatItems, line 506): +```swift +func apiShareChatMsgContent(shareChatType: ChatType, shareChatId: Int64, toSendRef: SendRef) async throws -> MsgContent { + let r: APIResult = await chatApiSendCmd( + .apiShareChatMsgContent(shareChatType: shareChatType, shareChatId: shareChatId, toSendRef: toSendRef) + ) + if case let .result(.chatMsgContent(_, mc)) = r { return mc } + throw r.unexpected +} +``` + +**apiConnectPlan** (line 1023-1032): add `linkOwnerSig: LinkOwnerSig? = nil` parameter, pass to `.apiConnectPlan(userId:connLink:linkOwnerSig:)`. + +### 4. `SimpleXChat/ChatUtils.swift` — NO new filter function + +Reuse `filterChatsToForwardTo` + `canForwardToChat` as-is. Chat cards ARE simplex links, so `prohibitedByPref(hasSimplexLink: true, ...)` applies and correctly gates by the destination's simplex-links preference + user's role. No separate filter. + +### 5. `ChatItemForwardingView.swift` — parameterize for dual use + +Add parameters to support both forwarding and sharing modes. **No new view file.** + +Add to the struct: +```swift +var title: String = "Forward" +var chats: [Chat] // caller provides filtered list (replaces internal filterChatsToForwardTo) +var isProhibited: ((Chat) -> Bool)? = nil // default: existing prohibitedByPref check; sharing overrides +var onSelect: (Chat) -> Void // replaces the inline tap handler +``` + +Remove `chatItems`, `fromChatInfo`, and `composeState` — the `onSelect` closure captures whatever the caller needs. The caller builds `ComposeState` and does navigation externally. + +Existing forwarding call site (`ChatView.swift:278`) adapts: +```swift +ChatItemForwardingView( + title: "Forward", + chats: filterChatsToForwardTo(chats: chatModel.chats), + isProhibited: { chat in forwardedChatItems.map { ci in chat.prohibitedByPref(...) }.contains(true) }, + onSelect: { chat in + dismiss forwarding sheet + set composeState to forwarding context + if different chat: loadOpenChat(chat.id) + } +) +``` + +Sharing call site (from GroupChatInfoView): +```swift +ChatItemForwardingView( + title: "Share channel", + chats: filterChatsToForwardTo(chats: chatModel.chats), // same filter + isProhibited: { chat in chat.prohibitedByPref(hasSimplexLink: true, isMediaOrFileAttachment: false, isVoice: false) }, + onSelect: { chat in + dismiss info sheet + set composeState to .sharingChatCard(sourceGroupInfo) + if different chat: loadOpenChat(chat.id) + } +) +``` + +### 6. `ContextItemView.swift` — parameterize for chat-card context + +Add an optional `customText: String?` property. When set, render that text instead of the ChatItem preview. Everything else (icon, cancel button, background, layout) stays the same. + +```swift +var customText: String? = nil // e.g., "Sharing #news" +``` + +When `customText != nil`: +- Display the string in place of the `msgContentView` / multi-message count +- Use `Color(uiColor: .tertiarySystemBackground)` for background (no ChatItem to derive color from) + +ComposeView's `contextItemView()` dispatch for the new case: +```swift +case let .sharingChatCard(sourceGroupInfo): + ContextItemView( + chat: chat, + contextItems: [], + contextIcon: "arrowshape.turn.up.forward", + cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }, + customText: "Sharing #\(sourceGroupInfo.groupProfile.displayName)" + ) + Divider() +``` + +### 7. `ComposeView.swift` — new context case + send dispatch + +**New `ComposeContextItem` case** (line 20-26): +```swift +case sharingChatCard(sourceGroupInfo: GroupInfo) +``` +Name is `sharingChatCard` (not channel-specific — MCChat is general). + +**Convenience init** (after `forwardingItems` init at 90-96): +```swift +init(sharingChatCard sourceGroupInfo: GroupInfo) { + self.message = "" + self.parsedMessage = [] + self.preview = .noPreview + self.contextItem = .sharingChatCard(sourceGroupInfo: sourceGroupInfo) + self.voiceMessageRecordingState = .noRecording +} +``` + +**Accessor** (after `forwarding` at 146-150): +```swift +var sharingChatCard: Bool { + switch contextItem { + case .sharingChatCard: return true + default: return false + } +} +``` + +**sendEnabled** (176): add `|| sharingChatCard`. + +**Draft-restore guard** (`ChatView.swift:758`): extend `!composeState.forwarding` to `!composeState.forwarding && !composeState.sharingChatCard`. + +**Send dispatch** in `sendMessageAsync` (before forwarding branch at 1354): +```swift +if case let .sharingChatCard(sourceGroupInfo) = composeState.contextItem { + sent = await shareChatCard(sourceGroupInfo, ttl) +} else if case let .forwardingItems(...) = ... { +``` + +**Helper** inside the same scope: +```swift +func shareChatCard(_ sourceGroupInfo: GroupInfo, _ ttl: Int?) async -> ChatItem? { + let toSendRef: SendRef + switch chat.chatInfo { + case let .direct(contact): + toSendRef = .direct(contactId: contact.contactId) + case let .group(gInfo, scope): + toSendRef = .group(groupId: gInfo.groupId, scope: scope, asGroup: gInfo.useRelays) + default: + return nil + } + do { + var mc = try await apiShareChatMsgContent( + shareChatType: .group, shareChatId: sourceGroupInfo.groupId, toSendRef: toSendRef + ) + // Append user-typed text: backend returns MCChat with text=link; prepend user message if present + if !composeState.message.isEmpty, case let .chat(text, chatLink, ownerSig) = mc { + mc = .chat(text: composeState.message + "\n" + text, chatLink: chatLink, ownerSig: ownerSig) + } + return await send(mc, quoted: nil, live: false, ttl: ttl, mentions: [:]) + } catch { + logger.error("shareChatCard failed: \(error.localizedDescription)") + return nil + } +} +``` + +**Post-send draft-restore** (1411-1417): mirror `wasForwarding` with `wasSharing`. + +### 8. `GroupChatInfoView.swift` — two entry points + composeState plumbing + +**Add to struct**: `@Binding var composeState: ComposeState` and `@State private var showSharePicker = false`. + +**Quick-access button** — in `infoActionButtons()` (line 354-370), add after `channelLinkActionButton` / `addMembersActionButton` branch: +```swift +if groupInfo.useRelays && groupInfo.groupProfile.publicGroup?.groupLink != nil { + InfoViewButton(image: "arrowshape.turn.up.forward", title: "share", width: buttonWidth) { + showSharePicker = true + } + .disabled(!groupInfo.ready) +} +``` +Adjust the `buttonWidth` divisor accordingly (4 → 5 if all four buttons can show). + +**Section button** — in `if groupInfo.useRelays` Section (line 104-125), after existing "Share link" button (115): +```swift +Button { + showSharePicker = true +} label: { + Label("Share via chat", systemImage: "arrowshape.turn.up.forward") +} +``` + +**Sheet** — on the body: +```swift +.sheet(isPresented: $showSharePicker) { + let shareChats = filterChatsToForwardTo(chats: ChatModel.shared.chats) + if #available(iOS 16.0, *) { + ChatItemForwardingView( + title: "Share channel", + chats: shareChats, + isProhibited: { $0.prohibitedByPref(hasSimplexLink: true, isMediaOrFileAttachment: false, isVoice: false) }, + onSelect: { chat in selectShareDestination(chat) } + ).presentationDetents([.fraction(0.8)]) + } else { + ChatItemForwardingView( + title: "Share channel", + chats: shareChats, + isProhibited: { $0.prohibitedByPref(hasSimplexLink: true, isMediaOrFileAttachment: false, isVoice: false) }, + onSelect: { chat in selectShareDestination(chat) } + ) + } +} +``` + +**selectShareDestination helper** in the same struct: +```swift +private func selectShareDestination(_ chat: Chat) { + showSharePicker = false + composeState = ComposeState(sharingChatCard: groupInfo) + if chat.id != ChatModel.shared.chatId { + ItemsModel.shared.loadOpenChat(chat.id) + } + dismiss() // dismiss info sheet too +} +``` + +**ChatView.swift:505-517**: pass `composeState: $composeState` to `GroupChatInfoView`. + +### 9. View rendering — `MsgContent.chat` text handling + +**On send**: text = `\n`. Link is the `strEncode groupLink` that the backend includes. If user typed nothing, text = just the link. + +**On display**: when rendering an `MCChat` message, strip the last line from `text` if it equals `chatLink`'s encoded link. This way: +- Old clients (no MCChat support) see text as-is: "hello\nhttps://simplex.chat/g#..." — usable. +- New clients (MCChat support) see "hello" + the rendered card — no redundant link. + +Implement in the card view's text rendering (§10 below). The stripping logic: +```swift +func chatCardText(_ text: String, _ chatLink: MsgChatLink) -> String { + let link = chatLinkStr(chatLink) + if text.hasSuffix("\n" + link) { + return String(text.dropLast(link.count + 1)) + } + return text +} +``` +Where `chatLinkStr` extracts the encoded link from the `MsgChatLink` variant. + +### 10. Card rendering — shared component from `CIGroupInvitationView` + +Extract a reusable **`CICardView`** from `CIGroupInvitationView`. This is a shared component that both views use (not a copy-paste). + +**`CICardView`** (new file `Shared/Views/Chat/ChatItem/CICardView.swift`): +Provides the outer frame: background with chat-tail padding, ZStack with bottomTrailing meta, VStack with: +- Header slot (profile image + name) +- Divider +- Body slot (action text, subtitle) + +Parameterized by: +```swift +struct CICardView: View { + @ObservedObject var chat: Chat + var chatItem: ChatItem + var header: Header + var body_: Body // avoid collision with View.body + var onTap: (() -> Void)? +} +``` + +**`CIGroupInvitationView`** refactored to use `CICardView`: +- Passes `groupInfoView(action)` as header +- Passes invitation text + "Tap to join" as body +- Passes `joinGroup(groupId)` as onTap +- All existing behaviour preserved (status checks, progress indicator, incognito) + +**`CIChatLinkView`** (new file, uses `CICardView`): +- Header: `ProfileImage` from `groupProfile.image` / `profile.image` + display name. Icon = same as chat list (no need to invent): + - `.group` channel (`publicGroup?.groupType == .channel`): `antenna.radiowaves.left.and.right.circle.fill` (from `GroupInfo.chatIconName` when `useRelays`) + - `.group` non-channel: `person.2.circle.fill` (from `GroupInfo.chatIconName` default) + - `.group` business: `briefcase.circle.fill` (from `GroupInfo.chatIconName` business case) + - `.contact` non-bot: `person.crop.circle.fill` (from `Contact.chatIconName`) + - `.contact` business address: `briefcase.circle.fill` + - `.invitation`: `person.crop.circle.fill` +- Body: stripped text (via `chatCardText`) + subtitle line: + - If `ownerSig != nil`: "signed" (secondary color) — same as CLI, it's a claim, verification happens on tap + - Action line: "Tap to open" (primary color) +- onTap (both sent and received): calls `planAndConnect(connLink, linkOwnerSig: ownerSig, ...)` — full connect flow with owner verification + +**Wire-in** at `ChatItemView.swift:73-90`, before the `isShortEmoji` check: +```swift +if case let .chat(_, chatLink, _) = ci.content.msgContent { + CIChatLinkView(chat: chat, chatItem: ci, chatLink: chatLink) +} else if let mc = ... { +``` + +### 11. Connect flow — owner verification in alerts + +**`planAndConnect`** (`NewChatView.swift:1218`): add optional `linkOwnerSig: LinkOwnerSig? = nil` parameter. Pass to `apiConnectPlan(connLink:linkOwnerSig:)`. + +**Alert text** — in the `.ok` branches of `planAndConnect` where the alert is built (lines 1255-1410), extend the alert body with owner verification info: +- `case .verified`: append "Channel owner signature verified." to alert message. +- `case .failed(let reason)`: append "Owner signature verification failed: \(reason)." to alert message. Consider making this a warning-styled alert. +- `nil` (no sig): no additional text. + +This surfaces in the standard connect-confirmation alert before the user taps "Connect" / "Join". + +### 12. Haskell — strip ownerSig on forward + +When a received MCChat message is forwarded (the existing forward-items path in `Library/Commands.hs`), the `dropSig` function already strips `ownerSig` for cross-chat forwarding (binding mismatch). The existing code at `Commands.hs:1000-1006` handles this. **No additional Haskell work for v1.** + +Card forwarding (subscriber shares the card further) naturally produces an unsigned card — the subscriber doesn't have the owner's key. This is correct. + +--- + +## C. Decisions — all resolved + +1. **Icon for "share" button**: `arrowshape.turn.up.forward` — easy to change later. +2. **Text stripping**: strip only the last line if it exactly matches the encoded link. User doesn't control the last line (backend appends it). If user also types the link, the typed copy remains — no special handling needed. +3. **"signed" label on card**: shown unconditionally when `ownerSig != nil`. It's a claim (same as CLI "signed"); verification happens on tap via `planAndConnect`. +4. **Card tap for sent items**: yes, same `planAndConnect` flow for both sent and received. +5. **Icons**: reuse existing `chatIconName` icons from chat list — `antenna.radiowaves.left.and.right.circle.fill` (channel), `person.2.circle.fill` (group), `briefcase.circle.fill` (business), `person.crop.circle.fill` (contact/invitation). Contact address sharing accounts for business address. + +--- + +## D. Items to lock during coding (not user decisions) + +- Exact `chatApiSendCmd` decode shape vs `chatSendCmd` / `processSendMessageCmd` for `apiShareChatMsgContent` response. +- `CICardView` exact slot API: whether to use `@ViewBuilder` closures or generic type params — decide during extraction from `CIGroupInvitationView` based on what minimises the diff. +- `planAndConnect` alert builder structure — may need a helper to format the owner-verification line, to be added inline during the `.ok` branch modifications. +- `chatLinkStr` extraction — how to get the encoded link string from `MsgChatLink` for text-stripping. Likely just `strEncode` equivalent on the `connLink` / `invLink` / `groupLink` field. diff --git a/plans/2026-04-17-kotlin-share-channel-link.md b/plans/2026-04-17-kotlin-share-channel-link.md new file mode 100644 index 0000000000..0133ca377c --- /dev/null +++ b/plans/2026-04-17-kotlin-share-channel-link.md @@ -0,0 +1,561 @@ +# Kotlin/Desktop — Share chat card (MCChat) — Implementation Plan + +Port of iOS commit `f49d98511` to Kotlin multiplatform codebase. Every section maps an iOS change to its Kotlin equivalent with file:line anchors. + +--- + +## 1. Types — `ChatModel.kt` + +### 1.1 Add `LinkOwnerSig` (new type, near line 4551 after `MsgChatLink`) + +```kotlin +@Serializable +data class LinkOwnerSig( + val ownerId: String? = null, + val chatBinding: String, + val ownerSig: String +) +``` + +iOS equivalent: `ChatTypes.swift` `LinkOwnerSig` struct. + +### 1.2 Add `ownerSig` to `MCChat` (line ~4310) + +Current: `class MCChat(override val text: String, val chatLink: MsgChatLink): MsgContent()` +Change to: `class MCChat(override val text: String, val chatLink: MsgChatLink, val ownerSig: LinkOwnerSig? = null): MsgContent()` + +### 1.3 Add `chatLinkStr` property to `MsgContent` (near `text` property) + +```kotlin +val chatLinkStr: String? + get() = (this as? MCChat)?.chatLink?.connLinkStr +``` + +### 1.4 Update `MsgContentSerializer` (lines 4366-4496) + +In the `"chat"` case of the deserializer, add `ownerSig` field: +```kotlin +"chat" -> { + val text = json["text"]?.jsonPrimitive?.content ?: "" + val chatLink = Json.decodeFromJsonElement(json["chatLink"]!!) + val ownerSig = json["ownerSig"]?.let { Json.decodeFromJsonElement(it) } + MCChat(text, chatLink, ownerSig) +} +``` + +In the serializer, add `ownerSig` to the `MCChat` case: +```kotlin +is MCChat -> buildJsonObject { + put("type", "chat") + put("text", mc.text) + put("chatLink", Json.encodeToJsonElement(mc.chatLink)) + mc.ownerSig?.let { put("ownerSig", Json.encodeToJsonElement(it)) } +} +``` + +### 1.5 Add computed properties to `MsgChatLink` (line ~4547) + +The existing `MsgChatLink` sealed class uses `@SerialName` annotations for JSON. The Haskell side uses `taggedObjectJSON` format (`{"type": "group", ...}`). Need to verify the existing `@SerialName` produces the right format — it should, since kotlinx.serialization with `classDiscriminator = "type"` matches. + +Add after the sealed class definition: +```kotlin +sealed class MsgChatLink { + // ... existing cases ... + + val isPublicGroup: Boolean + get() = (this as? Group)?.groupProfile?.publicGroup != null + + val connLinkStr: String + get() = when (this) { + is Group -> connLink + is Contact -> connLink + is Invitation -> invLink + } + + val image: String? + get() = when (this) { + is Group -> groupProfile.image + is Contact -> profile.image + is Invitation -> profile.image + } + + val displayName: String + get() = when (this) { + is Group -> groupProfile.displayName + is Contact -> profile.displayName + is Invitation -> profile.displayName + } + + val fullName: String + get() = when (this) { + is Group -> groupProfile.fullName + is Contact -> profile.fullName + is Invitation -> profile.fullName + } + + val shortDescription: String? + get() { + val s = when (this) { + is Group -> groupProfile.shortDescr + is Contact -> profile.shortDescr + is Invitation -> profile.shortDescr + } + return s?.trim()?.ifEmpty { null } + } + + val iconRes: ImageResource // for ProfileImage icon parameter + get() = when (this) { + is Group -> when (groupProfile.publicGroup?.groupType) { + GroupType.Channel -> MR.images.ic_bigtop_updates_padded + else -> MR.images.ic_supervised_user_circle_filled + } + is Contact -> if (business) MR.images.ic_work_filled_padded else MR.images.ic_account_circle_filled + is Invitation -> MR.images.ic_account_circle_filled + } + + val smallIconRes: ImageResource // for inline icon in context/quote views + get() = when (this) { + is Group -> when (groupProfile.publicGroup?.groupType) { + GroupType.Channel -> MR.images.ic_bigtop_updates + else -> MR.images.ic_group + } + is Contact -> if (business) MR.images.ic_work else MR.images.ic_person + is Invitation -> MR.images.ic_person + } + + fun infoLine(signed: Boolean): String { + var s = when (this) { + is Group -> when (groupProfile.publicGroup?.groupType) { + GroupType.Channel -> generalGetString(MR.strings.channel_link) + else -> generalGetString(MR.strings.group_link) + } + is Contact -> if (business) generalGetString(MR.strings.business_address) else generalGetString(MR.strings.contact_address) + is Invitation -> generalGetString(MR.strings.one_time_link) + } + if (signed) { + s += " " + if (isPublicGroup) generalGetString(MR.strings.from_owner) else generalGetString(MR.strings.signed_parentheses) + } + return s + } +} +``` + +Icons resolved — see "Resolved decisions" section 1. + +### 1.6 Add `OwnerVerification` type (near ConnectionPlan, line ~6844 of SimpleXAPI.kt) + +```kotlin +@Serializable +sealed class OwnerVerification { + @Serializable @SerialName("verified") object Verified : OwnerVerification() + @Serializable @SerialName("failed") class Failed(val reason: String) : OwnerVerification() +} +``` + +### 1.7 Update plan types with `ownerVerification` (SimpleXAPI.kt lines 6852-6877) + +- `InvitationLinkPlan.Ok`: add `val ownerVerification: OwnerVerification? = null` +- `ContactAddressPlan.Ok`: add `val ownerVerification: OwnerVerification? = null` +- `GroupLinkPlan.Ok`: add `val ownerVerification: OwnerVerification? = null` + +--- + +## 2. API commands — `SimpleXAPI.kt` + +### 2.1 Add `ApiShareChatMsgContent` command class (near line 3626) + +```kotlin +class ApiShareChatMsgContent( + val shareChatType: ChatType, val shareChatId: Long, + val toChatType: ChatType, val toChatId: Long, + val toScope: GroupChatScope?, val sendAsGroup: Boolean +): CC() +``` + +Add `cmdString`: +```kotlin +is ApiShareChatMsgContent -> { + val asGroup = if (sendAsGroup) "(as_group=on)" else "" + "/_share chat content ${chatRef(shareChatType, shareChatId)} ${chatRef(toChatType, toChatId, toScope)}$asGroup" +} +``` + +### 2.2 Add `CR.ChatMsgContent` response (near line 6320) + +```kotlin +@Serializable @SerialName("chatMsgContent") +class ChatMsgContent(val user: UserRef, val msgContent: MsgContent): CR() +``` + +### 2.3 Add `apiShareChatMsgContent` wrapper function (near line 1133) + +```kotlin +suspend fun apiShareChatMsgContent( + rh: Long?, shareChatType: ChatType, shareChatId: Long, + toChatType: ChatType, toChatId: Long, + toScope: GroupChatScope?, sendAsGroup: Boolean +): MsgContent? { + val r = sendCmd(rh, CC.ApiShareChatMsgContent(shareChatType, shareChatId, toChatType, toChatId, toScope, sendAsGroup)) + if (r is CR.ChatMsgContent) return r.msgContent + apiErrorAlert("apiShareChatMsgContent", r) + return null +} +``` + +### 2.4 Update `apiConnectPlan` (line 1488) + +Add `linkOwnerSig: LinkOwnerSig? = null` parameter. Update the `CC.APIConnectPlan` class to include it. Update cmdString to append `sig=` when present. + +--- + +## 3. Compose state — `ComposeView.kt` + `Enums.kt` + +### 3.1 Add `SharedContent.ChatLink` to `Enums.kt` (line 13-18) + +```kotlin +data class ChatLink(val groupInfo: GroupInfo): SharedContent() +``` + +This triggers the share flow: sets `chatModel.sharedContent.value = SharedContent.ChatLink(groupInfo)` → navigates to chat list → user picks destination. + +### 3.2 Add `ChatLinkPreview` to `ComposePreview` (`ComposeView.kt` line 57-63) + +```kotlin +@Serializable class ChatLinkPreview(val chatLink: MsgChatLink, val ownerSig: LinkOwnerSig?): ComposePreview() +``` + +### 3.3 Update `ComposeState` (`ComposeView.kt` line 103-240) + +- `sendEnabled`: add `is ComposePreview.ChatLinkPreview -> true` case +- `linkPreviewAllowed`: add `is ComposePreview.ChatLinkPreview -> false` +- `attachmentPreview`: add `is ComposePreview.ChatLinkPreview -> false` + +### 3.4 Add compose preview rendering + +In the compose area where previews are rendered, add a case for `ChatLinkPreview` that shows `ComposeChatLinkView` (new composable). + +### 3.5 Add send handling + +In the send function, add case for `ChatLinkPreview`: +```kotlin +is ComposePreview.ChatLinkPreview -> { + val linkStr = preview.chatLink.connLinkStr + val text = if (msgText.isEmpty()) linkStr else "$msgText\n$linkStr" + send(MsgContent.MCChat(text, preview.chatLink, preview.ownerSig), ...) +} +``` + +### 3.6 Handle `SharedContent.ChatLink` in `ComposeView.kt` (line 1431-1446, `LaunchedEffect(chatModel.sharedContent.value)`) + +When the destination chat opens with `SharedContent.ChatLink`, the `LaunchedEffect` fires. At this point: +- `chatModel.chatId.value` = destination chat ID +- `shared.groupInfo` = source group (what we're sharing) +- The current chat's `ChatInfo` provides destination type/id/scope for the API call + +```kotlin +is SharedContent.ChatLink -> { + // chat variable is available in ComposeView scope — it's the destination chat + val cInfo = chat.chatInfo + val sendAsGroup = cInfo.groupInfo?.let { it.useRelays && it.membership.memberRole >= GroupMemberRole.Owner } ?: false + withBGApi { + val mc = chatModel.controller.apiShareChatMsgContent( + chat.remoteHostId, ChatType.Group, shared.groupInfo.groupId, + cInfo.chatType, cInfo.apiId, + cInfo.groupChatScope(), sendAsGroup + ) + if (mc is MsgContent.MCChat) { + composeState.value = composeState.value.copy( + preview = ComposePreview.ChatLinkPreview(mc.chatLink, mc.ownerSig) + ) + } else if (mc != null) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.error_sharing_channel), + mc.toString() + ) + } + } +} +``` + +Note: `chat` is available as a parameter in the ComposeView composable scope. `withBGApi` is needed because `apiShareChatMsgContent` is a suspend function and `LaunchedEffect` already runs in a coroutine but the API call should use the standard error handling pattern. + +### 3.7 Handle `SharedContent.ChatLink` in `ShareListView.kt` (line 33-54) + +Add filtering case in the `when (sharedContent)` block: +```kotlin +is SharedContent.ChatLink -> { + hasSimplexLink = true // chat cards ARE simplex links, prohibited by SimplexLinks group pref +} +``` + +This means in `ShareListNavLinkView` (line 44): `simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)` — groups where simplex links are disabled will show as prohibited (disabled row + alert on tap). Direct chats and local notes are unaffected (line 30-31 don't check simplex links for direct). + +### 3.8 Handle `SharedContent.ChatLink` in `ShareListNavLinkView.kt` (line 28-67) + +The existing `when (chat.chatInfo)` dispatch handles click actions per chat type. For `SharedContent.ChatLink`, the click action (line 37 `directChatAction`, line 54 `groupChatAction`) opens the destination chat. `ComposeView`'s `LaunchedEffect` (§3.6) then picks up the `SharedContent.ChatLink` and sets up the compose preview. + +No changes needed to `ShareListNavLinkView` click handlers — they already open the correct chat. The `SharedContent.ChatLink` is consumed by `ComposeView`. + +### 3.9 Handle `SharedContent.ChatLink` in `ShareListToolbar` (line 142-147) + +Add title for the share list toolbar: +```kotlin +is SharedContent.ChatLink -> stringResource(MR.strings.share_channel) +``` + +### 3.10 Handle back navigation from share list with `SharedContent.ChatLink` (line 126-133) + +When user taps back on the share list with `SharedContent.ChatLink`, should navigate back to the source chat (like Forward navigates back to `fromChatInfo.id`): +```kotlin +if (sharedContent is SharedContent.ChatLink) { + chatModel.chatId.value = sharedContent.groupInfo.id +} +``` + +--- + +## 4. New composables + +### 4.1 `ComposeChatLinkView.kt` (new file) + +Near `ComposeView.kt`. Shows ProfileImage + displayName + optional shortDescription. Cancel button. Mirrors iOS `ComposeChatLinkView`. + +### 4.2 `CIChatLinkHeader.kt` (new file) + +Near `FramedItemView.kt`. Shows profile header (image + name + fullName), shortDescription, info line, "Tap to open" + meta. Mirrors iOS `CIChatLinkHeader`. + +--- + +## 5. Message rendering — `FramedItemView.kt` + +### 5.1 Add `MCChat` case in content dispatch (line ~296-341) + +After the `MCLink` case: +```kotlin +is MsgContent.MCChat -> { + val hasText = mc.text != mc.chatLink.connLinkStr + CIChatLinkHeader(chatItem = ci, chatLink = mc.chatLink, ownerSig = mc.ownerSig, hasText = hasText) + // tap gesture → planAndConnect(mc.chatLink.connLinkStr, linkOwnerSig = mc.ownerSig) + if (hasText) { + CIMarkdownText(..., stripLink = mc.chatLink.connLinkStr) + } +} +``` + +### 5.2 Add `MCChat` case in quote dispatch (line ~142-183) + +```kotlin +is MsgContent.MCChat -> { + val prefix = buildAnnotatedString { + append(mc.chatLink.displayName) + append(if (mc.text != mc.chatLink.connLinkStr) " - " else "") + } + CIQuotedMsgView(qi, stripLink = mc.chatLink.connLinkStr, prefix = prefix) + // + small icon +} +``` + +### 5.3 Add `stripLink` parameter to text rendering + +`CIMarkdownText` / `MarkdownText` (TextItemView.kt) needs `stripLink: String? = null` parameter. Inside, strip the text and formattedText before rendering. + +Add `stripTextLink` and `stripFormattedTextLink` functions near `MarkdownText`: + +```kotlin +fun stripTextLink(text: String, link: String): String = + if (text == link) "" + else if (text.endsWith("\n$link")) text.dropLast(link.length + 1) + else text + +fun stripFormattedTextLink(ft: List?, link: String): List? { + if (ft == null || ft.isEmpty() || ft.last().text != link) return ft + val result = ft.toMutableList() + result.removeLast() + val i = result.lastIndex + if (i >= 0 && result[i].format == null && result[i].text.endsWith("\n")) { + result[i] = result[i].copy(text = result[i].text.dropLast(1)) + if (result[i].text.isEmpty()) result.removeLast() + } + return result.ifEmpty { null } +} +``` + +--- + +## 6. Chat list preview — `ChatPreviewView.kt` + +### 6.1 Add content preview for `MCChat` (line ~293-337) + +```kotlin +is MsgContent.MCChat -> { + SmallContentPreview(borderColor = if (mc.chatLink.image != null) ...) { + ProfileImage(mc.chatLink.image, mc.chatLink.iconName, size) + // onClick → planAndConnect + } +} +``` + +### 6.2 Update text preview (line ~217-290) + +For `MCChat`, show `displayName + description` instead of raw text: +```kotlin +is MsgContent.MCChat -> { + val descr = mc.chatLink.shortDescription?.let { "\n$it" } ?: "" + itemText = mc.chatLink.displayName + descr + formattedText = null +} +``` + +--- + +## 7. Context/forwarding view — `ContextItemView.kt` + +### 7.1 Add `MCChat` attachment icon (line 75-84, `fun attachment()`) + +```kotlin +is MsgContent.MCChat -> mc.chatLink.smallIconRes +``` + +This returns the small icon (e.g., `MR.images.ic_bigtop_updates` for channels). The icon is rendered inline via the existing `inlineContent` mechanism in `MessageText` (line 42-58). + +### 7.2 Add `MCChat` case in `ContextMsgPreview` or `MessageText` (line 87-89) + +For MCChat, `MessageText` needs: +1. The attachment icon (from §7.1) — rendered inline by existing mechanism +2. `prefix` with `chatLink.displayName + " - "` (or just displayName if no text) — `MarkdownText` already has `prefix: AnnotatedString?` +3. `stripLink = chatLink.connLinkStr` — strips the link from text + +Modify `ContextMsgPreview` (line 87-89) or add a special case: +```kotlin +fun ContextMsgPreview(contextItem: ChatItem, lines: Int) { + val mc = contextItem.content.msgContent + if (mc is MsgContent.MCChat) { + val hasText = contextItem.text != mc.chatLink.connLinkStr + val prefix = buildAnnotatedString { append(mc.chatLink.displayName + if (hasText) " - " else "") } + MessageText(contextItem, mc.chatLink.smallIconRes, lines, prefix = prefix, stripLink = mc.chatLink.connLinkStr) + } else { + MessageText(contextItem, remember(contextItem.id) { attachment(contextItem) }, lines) + } +} +``` + +This requires `MessageText` to accept optional `prefix` and `stripLink` parameters and pass them to `MarkdownText`. + +--- + +## 8. Group link / info views + +### 8.1 `GroupLinkView.kt` (line ~27) + +Add parameter: `groupInfo: GroupInfo? = null`. +Add "Share via chat" button when `groupInfo?.groupProfile?.publicGroup != null`. +Button action: `chatModel.sharedContent.value = SharedContent.ChatLink(groupInfo)` + close modals + navigate to chat list. + +### 8.2 `GroupChatInfoView.kt` + +Add "Share via chat" button in the channel link section (next to existing "Share link" button). +Button action: same as 8.1 — sets `SharedContent.ChatLink` and navigates. + +No `composeState` parameter needed (unlike iOS) — the `SharedContent` pattern handles state transfer without bindings. + +### 8.3 Channel creation (equivalent of `AddChannelView`) + +Find the Kotlin channel creation flow and pass `groupInfo` to `GroupLinkView` so "Share via chat" is available during creation. + +### 8.4 Share flow summary (no separate `shareChatLink` function needed) + +Unlike iOS which has a separate `shareChatLink` free function (due to sheet-based navigation), Kotlin's flow is: + +1. User taps "Share via chat" → `chatModel.sharedContent.value = SharedContent.ChatLink(groupInfo)` + `chatModel.chatId.value = null` (navigates to chat list showing `ShareListView`) +2. `ShareListView` shows filtered chats with `hasSimplexLink = true` prohibition +3. User picks destination → `directChatAction`/`groupChatAction` opens the chat +4. `ComposeView`'s `LaunchedEffect` fires (§3.6) → calls `apiShareChatMsgContent` → sets `ComposePreview.ChatLinkPreview` +5. User types optional text, taps Send +6. Send dispatch (§3.5) constructs `MCChat(text + link, chatLink, ownerSig)` and sends + +The API call happens in `ComposeView`'s `LaunchedEffect`, not in a separate function. Error handling: if the API fails, show alert and clear `sharedContent`. + +For the **channel creation flow** (no chat open yet): when `SharedContent.ChatLink` is consumed in `ComposeView` and the API call succeeds, the preview is set directly. No draft fallback needed — the chat IS already open at that point (the user picked it from the share list). + +--- + +## 9. Connect flow + +### 9.1 Update `planAndConnect` equivalent + +Add `linkOwnerSig: LinkOwnerSig? = null` parameter. Pass to `apiConnectPlan`. Thread `ownerVerification` from plan result to connect alerts. + +### 9.2 Add `ownerVerificationMessage` function + +```kotlin +fun ownerVerificationMessage(ov: OwnerVerification?): String? = when (ov) { + is OwnerVerification.Verified -> generalGetString(MR.strings.link_signature_verified) + is OwnerVerification.Failed -> "⚠️ " + String.format(generalGetString(MR.strings.signature_verification_failed), ov.reason) + null -> null +} +``` + +### 9.3 Update connect alerts + +Add `information: String? = null` parameter to `AlertManager.showOpenChatAlert` (`AlertManager.kt` line 271). Render as a separate `Text` below subtitle with `MaterialTheme.colors.onSurface` color (not secondary — more prominent). + +Update `showPrepareContactAlert` (ConnectPlan.kt line 572) and `showPrepareGroupAlert` (line 612) to accept and pass `ownerVerification`. Thread from `planAndConnectTask` `.Ok` cases. + +--- + +## 10. String resources + +Add to `strings.xml` (all platforms). No collisions found with existing keys: +- `chat_link_channel` = "Channel link" +- `chat_link_group` = "Group link" +- `chat_link_business_address` = "Business address" +- `chat_link_contact_address` = "Contact address" +- `chat_link_one_time` = "One-time link" +- `chat_link_from_owner` = "(from owner)" +- `chat_link_signed` = "(signed)" +- `owner_verification_passed` = "Link signature verified." +- `owner_verification_failed` = "⚠️ Signature verification failed: %s." +- `error_sharing_channel` = "Error sharing channel" +- `share_via_chat` = "Share via chat" +- `share_channel` = "Share channel" +- `tap_to_open` = "Tap to open" + +--- + +## Resolved decisions (from investigation) + +### 1. Icon resource names (verified from `ChatModel.kt` and MR/images/) +- **Channel**: `MR.images.ic_bigtop_updates_padded` (used in `GroupInfo.chatIconName` when `useRelays`) +- **Group**: `MR.images.ic_supervised_user_circle_filled` (used in `GroupInfo.chatIconName` default) +- **Business**: `MR.images.ic_work_filled_padded` (used in `GroupInfo.chatIconName` business case) +- **Contact**: `MR.images.ic_account_circle_filled` (used in `Contact.chatIconName`) +- **Small (inline text) icons**: `MR.images.ic_bigtop_updates` (channel), `MR.images.ic_group` (group), `MR.images.ic_work` (business), `MR.images.ic_person` (contact/invitation) + +### 2. MsgChatLink JSON serialization +Existing `@Serializable` sealed class with `@SerialName` annotations already produces `{"type": "group", ...}` format. No custom serializer needed (confirmed by user). Keep existing pattern. + +### 3. Forwarding/sharing picker pattern +Kotlin uses `SharedContent` + navigate to chat list, NOT a sheet picker: +- Forward: sets `chatModel.sharedContent.value = SharedContent.Forward(items, fromInfo)` + `chatModel.chatId.value = null` (returns to chat list) +- Share: add `SharedContent.ChatLink(groupInfo: GroupInfo)` case → sets `sharedContent` → user picks chat from `ShareListView` → opens chat with `ChatLinkPreview` in compose + +`ShareListView.kt` (line 44) dispatches on `SharedContent` type for filtering. Add `SharedContent.ChatLink` case there with `hasSimplexLink = true` filtering. + +### 4. Navigation after share +- `ModalManager.closeAllModalsEverywhere()` dismisses all modals +- Setting `chatModel.chatId.value = chatId` navigates to a chat +- For the share flow: `shareChatLink` calls API → on success → `ModalManager.closeAllModalsEverywhere()` → sets `composeState` preview → sets `chatModel.chatId.value = destChat.id` + +### 5. Draft mechanism (verified at `ChatModel.kt:203-204`) +Same as iOS: `chatModel.draft: MutableState` and `chatModel.draftChatId: MutableState`. Used in `ComposeView.kt:435-444` for save/restore. Same fallback pattern as iOS for the channel creation flow. + +### 6. `planAndConnect` (verified at `ConnectPlan.kt:24-48`) +Single function `suspend fun planAndConnect(rhId, shortOrFullLink, close, cleanup, filterKnownContact, filterKnownGroup)` in `ConnectPlan.kt`. Add `linkOwnerSig: LinkOwnerSig? = null` parameter. Thread to `apiConnectPlan`. Thread `ownerVerification` to alert functions. + +Alert functions: `showPrepareContactAlert` (line 572) and `showPrepareGroupAlert` (line 612) use `AlertManager.privacySensitive.showOpenChatAlert(...)` which has `subtitle: String?`. Add `information: String? = null` parameter. + +### 7. Forwarding view parameterization +No `ChatItemForwardingView` to parameterize — Kotlin uses `ShareListView` which dispatches on `SharedContent` type. Add a new `SharedContent.ChatLink` case. `ShareListView` filters chats and shows the list. When user picks a chat, `ComposeView` reads `sharedContent` and sets compose state accordingly (line 1439: `is SharedContent.Forward -> composeState.value = ...`). Add handling for `SharedContent.ChatLink`. + +### 8. String resource naming +Need to check existing strings to avoid collisions. Use `chat_link_channel`, `chat_link_group`, etc. prefix pattern to avoid collision with existing `group_link` string. diff --git a/plans/2026-04-19-directory-public-groups.md b/plans/2026-04-19-directory-public-groups.md new file mode 100644 index 0000000000..1b23234d14 --- /dev/null +++ b/plans/2026-04-19-directory-public-groups.md @@ -0,0 +1,324 @@ +# Directory Service — Public Group Registration via Chat Cards + +## Goal + +Enable directory registration of public groups (channels and future group types) via MCChat cards shared in DM with the bot. Replaces the admin-invitation flow with a signature-verified card flow. + +## Background + +### Current group registration flow +1. Owner invites bot as admin member +2. Bot joins, creates group link, asks owner to add link to welcome message +3. Owner updates profile with link → bot sends for admin approval +4. Admin approves → group listed + +This requires the bot to be admin. Public groups don't need this — they already have a public link, and ownership is proven via `ownerSig` on the MCChat card. + +### Public group identity +- `PublicGroupProfile {groupType :: GroupType, groupLink :: ShortLinkContact, publicGroupId :: B64UrlByteString}` +- `publicGroupId = sha256(rootKey)` — immutable identity +- `GroupType`: currently `GTChannel`, adding `GTGroup` for forward compatibility +- `GroupKeys {publicGroupId, groupRootKey, memberPrivKey}` — owner's signing keys +- `ownerId` in `LinkOwnerSig` = `B64UrlByteString (unMemberId memberId)` — the owner's MemberId bytes + +### ownerId-to-member mapping +- `LinkOwnerSig.ownerId = Just (B64UrlByteString unMemberId)` — same raw bytes as `MemberId` +- `createLinkOwnerMember` (called during `APIConnectPreparedGroup`, Commands.hs:2129) creates a member record with `memberRole = GROwner`, `memberStatus = GSMemUnknown`, `memberContactId = Nothing` +- `GroupMemberId` is available immediately after `APIConnectPreparedGroup` +- `getGroupMemberIdViaMemberId db user gInfo (MemberId ownerId)` looks up `GroupMemberId` from `MemberId` + +### Owner member activation +When a relay announces the pre-created `GSMemUnknown` member, `CEvtUnknownMemberAnnounced` fires (Subscriber.hs:2872, via `xGrpMemNew`). The member's profile and role are updated from the announcement's `MemberInfo` (via `updateUnknownMemberAnnounced`, Groups.hs:3010) — the role reflects the member's actual current role, not the pre-created `GROwner`. This event is not currently handled in directory Events.hs. + +### connectPlan and known groups +`apiConnectPlan` with `linkOwnerSig` returns: +- `GLPOk {groupSLinkData_, ownerVerification}` — new group +- `GLPKnown {groupInfo}` — bot already a member +- `GLPOwnLink` / `GLPConnectingProhibit` / `GLPConnectingConfirmReconnect` / `GLPNoRelays` + +**Gap**: For `GLPKnown`, `groupShortLinkPlan` short-circuits via `knownLinkPlans` — never resolves link data, never verifies signature. + +**Fix**: Add an optional parameter to `APIConnectPlan` (before `sig=`, since JSON must be last) that forces link data re-resolution even for known groups. With this parameter, `GLPKnown` includes `ownerVerification` and freshly loaded `groupSLinkData`. The loaded profile may differ from stored — the bot treats the server's current data as authoritative and updates its stored profile accordingly. + +**Future**: Add a signed version counter to link data to detect rollback attacks (malicious server serving old signed profiles). The bot would store the highest version seen and reject/flag version reductions. For now, the server is treated as authoritative. + +### Owner-contact association via APIConnectPreparedGroup +`createLinkOwnerMember` (called during `APIConnectPreparedGroup`) currently creates owner members with `memberContactId = Nothing`. Add an optional `(contactId, ownerId)` paired parameter to `APIConnectPreparedGroup`: when the link was received in a DM, pass the sender's `contactId` and the `ownerId` from `LinkOwnerSig`. The core sets `memberContactId` on the specific owner member whose `memberId` matches `ownerId`. + +This makes ALL existing directory event routing work: `DEContactRoleChanged`, `DEContactRemovedFromGroup`, `DEContactLeftGroup` all resolve via `memberContactId` — no new event types needed for owner tracking. + +Also benefits regular UI: when a user taps an owner's link in a DM, the contact association is created, improving the experience (e.g., showing the contact in the group member list). + +## Registration flow for public groups + +1. Owner taps "Share via chat" on their public group → sends MCChat card to bot in DM +2. Bot receives `CEvtNewChatItems` with `MCChat` content in direct chat → `DEChatLinkReceived` +3. Bot validates card (see validation matrix) +4. Bot calls `apiConnectPlan` with `connLink`, `linkOwnerSig`, and force-resolve flag +5. On `GLPOk` + `Verified`: bot replies "Joining {channel/group} {name}..." and joins via `APIPrepareGroup` then `APIConnectPreparedGroup` (passing owner's `contactId` and `ownerId`). On error: replies "Error joining {channel/group} {name}, please re-send the link!" (same pattern as existing group flow, Service.hs:368-370). +6. After `APIConnectPreparedGroup`, bot stores `dbOwnerMemberId` (via `getGroupMemberIdViaMemberId` — `createLinkOwnerMember` created the record during connect). Registration status: `GRSProposed`. +7. When `CEvtUnknownMemberAnnounced` fires for the owner member → `DEOwnerMemberAnnounced` → bot transitions to `GRSPendingApproval`, replies "Joined {channel/group} {name}. Registration is pending approval — it may take up to 48 hours.", sends to admins for approval +8. Admin approves → `GRSActive` + +## Scenario matrix: card received in DM + +### Event + +One event: `DEChatLinkReceived { contact :: Contact, chatItemId :: ChatItemId, chatLink :: MsgChatLink, ownerSig :: Maybe LinkOwnerSig }`. + +Handler validates and replies based on content. + +### Card validation (handler level) + +| Condition | Action | +|---|---| +| `chatLink` is not MCLGroup, or MCLGroup but no `publicGroup` in profile | Reply: "Only channels can be added to directory via link." | +| MCLGroup + publicGroup but `ownerSig` is `Nothing` | Reply: "To add a {channel/group} to directory you must be the owner." | +| MCLGroup + publicGroup + `ownerSig` is `Just` | Proceed to connectPlan | + +### connectPlan results + +| Plan result | ownerVerification | Action | +|---|---|---| +| `GLPOk` + sLinkData | `Verified` | Reply "Joining {channel/group} {name}...", join (with contactId + ownerId), register as `GRSProposed` | +| `GLPOk` + sLinkData | `Failed reason` | Reply: "Link signature verification failed: {reason}.\nYou must be the {channel/group} owner to register it." | +| `GLPOk` + sLinkData | `Nothing` | Reply: "Error: could not verify {channel/group} ownership. Please report it to directory admins." | +| `GLPOk` no sLinkData | — | Reply: "Error: no {channel/group} information available via the link." | +| `GLPKnown` | `Verified` | Bot already member — handle as re-registration (see below) | +| `GLPKnown` | `Failed reason` | Reply: "Link signature verification failed: {reason}.\nYou must be the {channel/group} owner to register it." | +| `GLPKnown` | `Nothing` | Reply: "Error: could not verify ownership." | +| `GLPConnectingProhibit` | — | Reply: "Already connecting to this {channel/group}." | +| `GLPConnectingConfirmReconnect` | — | Reply: "Already connecting to this {channel/group}." | +| `GLPOwnLink` | — | Log error. Reply: "Unexpected error. Please report it to directory admins." | +| `GLPNoRelays` | — | Reply: "{Channel/Group} has no active relays. Please try again later." | + +### Owner member activation after joining + +Bot is in `GRSProposed`. The pre-created owner member has `GSMemUnknown` status. When the relay announces this member, `CEvtUnknownMemberAnnounced` fires → mapped to `DEOwnerMemberAnnounced` in directory events. + +| Condition | Action | +|---|---| +| `CEvtUnknownMemberAnnounced` for member matching `dbOwnerMemberId`, announced role is `GROwner` | Transition to `GRSPendingApproval`, notify submitting contact, send for admin approval | +| `CEvtUnknownMemberAnnounced` for member matching `dbOwnerMemberId`, announced role < `GROwner` | Reply: "The signing key does not belong to a current owner. Registration cancelled." Set `GRSRemoved`. | +| Owner member never announced | Registration stays in `GRSProposed`. No timeout — manual cleanup via admin. | + +### Re-registration (GLPKnown — bot already member, signature verified at plan) + +With the `connectPlan` fix, `GLPKnown` now includes `ownerVerification` and fresh `groupSLinkData`. Only proceed if `Verified`. + +Bot extracts `ownerId`, looks up member via `getGroupMemberIdViaMemberId`, confirms `memberRole >= GROwner` AND `memberStatus` is active (not `GSMemUnknown`). The pre-created member has `GROwner` role from creation, so role alone is insufficient — the member must have been announced by a relay to confirm actual presence in the group. + +Look up existing `GroupReg` by `groupId`: + +| Existing registration | Ownership verified | Action | +|---|---|---| +| No GroupReg found | Yes | Create new registration as `GRSPendingApproval` | +| GroupReg exists, same owner contact | Yes | Handle based on current status (see status matrix) | +| GroupReg exists, different contact | Sender is verified owner AND previous registrant no longer owner (check `dbOwnerMemberId` member's current role) | Transfer: update `dbContactId` and `dbOwnerMemberId`, proceed as same-owner case | +| GroupReg exists, different contact | Sender is verified owner BUT previous registrant still owner | Reply: "This {channel/group} is registered by another owner." | +| GroupReg exists, different contact | Sender NOT verified owner | Reply: "You must be the {channel/group} owner to register it." Additionally: check if previous registrant (via `dbOwnerMemberId`) is still owner. If not → suspend (`GRSSuspendedBadRoles`). | + +### Re-registration by same owner — status matrix + +| Current status | Action | +|---|---| +| `GRSProposed` | Only if owner member is active (not `GSMemUnknown`): transition to `GRSPendingApproval`, send for approval. If still `GSMemUnknown`: reply "Waiting for owner to connect to the {channel/group}." | +| `GRSPendingConfirmation` | Transition to `GRSPendingApproval`, send for approval (only if previously registered via admin-invitation flow) | +| `GRSPendingUpdate` | Transition to `GRSPendingApproval`, send for approval (only if previously registered via admin-invitation flow) | +| `GRSPendingApproval n` | Check if profile changed (fresh profile from connectPlan vs bot's current DB). If yes: increment approval ID, re-send. If no: reply "Already pending approval." | +| `GRSActive` | Check if profile changed. If yes: transition to `GRSPendingApproval`, re-send. If no: reply "Already listed in the directory." | +| `GRSSuspended` | Reply: "{Channel/Group} is suspended by admin. Contact support." | +| `GRSSuspendedBadRoles` | Ownership re-verified at plan. Transition to `GRSPendingApproval`, send for approval. | +| `GRSRemoved` | Re-register as `GRSPendingApproval` | + +### Profile change detection + +For re-registration: compare the freshly loaded profile (from connectPlan's re-resolved `groupSLinkData`) against the group's current profile in the bot's database. + +For XGrpInfo updates: re-resolve the link via `apiConnectPlan` with `resolve=on`, compare freshly loaded link profile against bot's stored profile. + +Uses the same `sameProfile` comparison as existing group flow (Service.hs:491-494), extended with `publicGroup` field: `displayName`, `fullName`, `shortDescr`, `image`, `description`, `memberAdmission`, `publicGroup` — any difference triggers re-approval. The `publicGroup` field includes `groupLink` (ShortLinkContact), so link regeneration by the owner also triggers re-approval. + +## Profile updates via XGrpInfo (bot is subscriber) + +Bot receives `DEGroupUpdated` when any member updates the group profile. Works for subscribers. + +For public groups: skip "link in welcome message" check. First check if the profile actually changed using the same `sameProfile` comparison as for regular groups (`displayName`, `fullName`, `shortDescr`, `image`, `description`, `memberAdmission`). Only if changed, call `apiConnectPlan` with `resolve=on` to re-resolve the link data. Compare the resolved link profile against the bot's stored profile. + +Note: `xGrpInfo` (Subscriber.hs:3172) prevents `publicGroup` removal and `publicGroupId` changes for channels — these cases can never occur. The `groupLink` (ShortLinkContact) CAN change if the owner regenerates the link; the bot's DB is updated via XGrpInfo and subsequent re-resolution uses the current link. + +| Current status | Profile changed (link data vs stored) | Action | +|---|---|---| +| `GRSProposed` | Any | No action (waiting for owner activation) | +| `GRSPendingApproval n` | Yes | Increment approval ID, re-send for approval | +| `GRSPendingApproval n` | No | No action | +| `GRSActive` | Yes | Transition to `GRSPendingApproval`, notify owner, re-send | +| `GRSActive` | No | No action | +| `GRSSuspended` | Any | No action | +| `GRSSuspendedBadRoles` | Any | No action | +| `GRSRemoved` | Any | No action | + +## Owner tracking + +### Owner-contact association + +When the bot connects via `APIConnectPreparedGroup` with the submitting contact's `contactId` and `ownerId`, the core sets `memberContactId` on the specific pre-created owner member whose `memberId` matches `ownerId`. This makes all existing event routing work: `DEContactRoleChanged`, `DEContactRemovedFromGroup`, `DEContactLeftGroup` resolve via `memberContactId`. + +### Owner changes + +| Event | Detection | Action | +|---|---|---| +| Owner loses owner role | `DEContactRoleChanged` (works via `memberContactId` set at connect time) | Transition to `GRSSuspendedBadRoles`, notify | +| Owner leaves group | `DEContactLeftGroup` | Transition to `GRSRemoved`, notify, leave group | +| Owner removed from group | `DEContactRemovedFromGroup` | Transition to `GRSRemoved`, notify, leave group | +| Non-owner sends card, current registrant no longer owner | Re-registration flow detects stale ownership | Suspend (`GRSSuspendedBadRoles`). Non-owner's card also checked: if their `ownerId` resolves to a non-owner member, and the current registrant is also not owner → suspend. | +| New owner sends card, current registrant no longer owner | Re-registration flow, verified | Transfer registration | + +## Commands for public group registrations + +Bot is subscriber (not admin): +- `/filter` — Reply: "This command is not available for public groups." +- `/role` — Reply: "This command is not available for public groups." +- `/link` — Show `PublicGroupProfile.groupLink` with appropriate message. +- `/delete` — Remove registration, bot leaves group (`APILeaveGroup`). +- `/list` — Works as before, includes public group registrations. + +## De-registration + +| Event | Action | +|---|---| +| Owner sends `/delete ID:NAME` | Delete registration, reply confirmation, leave group | +| Bot removed (`DEServiceRemovedFromGroup`) | Set `GRSRemoved`, notify | +| Group deleted (`DEGroupDeleted`) | Set `GRSRemoved`, notify | +| Owner leaves (`DEContactLeftGroup`) | Set `GRSRemoved`, notify, leave group | +| Owner removed (`DEContactRemovedFromGroup`) | Set `GRSRemoved`, notify, leave group | +| Admin sends `/suspend ID:NAME` | Set `GRSSuspended`, notify, do NOT leave group | + +Bot leaves group only for public group registrations (regular groups preserve existing behavior). + +## Code changes + +### 1. GroupType — add GTGroup + +`Types.hs`: +```haskell +data GroupType = GTChannel | GTGroup | GTUnknown Text +``` + +### 2. connectPlan — force-resolve parameter + +Add optional parameter to `APIConnectPlan` (before `sig=`): `resolve=on`. When present, `groupShortLinkPlan` skips the `knownLinkPlans` shortcut and always resolves link data. `GLPKnown` extended with `ownerVerification` and `groupSLinkData_`: +```haskell +GLPKnown {groupInfo :: GroupInfo, ownerVerification :: Maybe OwnerVerification, groupSLinkData_ :: Maybe GroupShortLinkData} +``` + +Parser: `/_connect plan [resolve=on] [sig=]` + +### 3. APIConnectPreparedGroup — optional (contactId, ownerId) + +Add optional paired `(contactId, ownerId)` parameter to `APIConnectPreparedGroup`. When present, `createLinkOwnerMember` (called during connect, Commands.hs:2129) sets `memberContactId` on the specific owner member whose `memberId` matches the provided `ownerId`. + +Current parser (Commands.hs:5045): `/_connect group # [incognito=on] []` +New parser: `/_connect group # [contact= owner=] [incognito=on] []` + +`contact` and `owner` are paired — both required together. `ownerId` identifies which pre-created owner member gets the `memberContactId` set (multiple owners possible via OwnerAuth chain). + +Current type (Controller.hs:479): `APIConnectPreparedGroup GroupId IncognitoEnabled (Maybe MsgContent)` +New type: `APIConnectPreparedGroup GroupId (Maybe (ContactId, B64UrlByteString)) IncognitoEnabled (Maybe MsgContent)` + +This also benefits the UI: when tapping an owner's link in a DM, the contactId is threaded through the connect alert to `APIConnectPreparedGroup`, creating the association. + +### 4. Events.hs — new events + +`DEChatLinkReceived` — fires for ALL MCChat messages in DM (any `MsgChatLink` variant, signed or unsigned): +```haskell +| DEChatLinkReceived + { contact :: Contact, + chatItemId :: ChatItemId, + chatLink :: MsgChatLink, + ownerSig :: Maybe LinkOwnerSig + } +``` + +`DEOwnerMemberAnnounced` (from `CEvtUnknownMemberAnnounced`): +```haskell +| DEOwnerMemberAnnounced GroupInfo GroupMember GroupMember + -- ^ groupInfo, unknownMember, announcedMember +``` + +In `crDirectoryEvent_`, extend `CEvtNewChatItems` for direct chat: +```haskell +(MCChat {chatLink, ownerSig}, Nothing) -> DEChatLinkReceived ct ciId chatLink ownerSig +``` + +Add `CEvtUnknownMemberAnnounced` handler: +```haskell +CEvtUnknownMemberAnnounced {groupInfo, unknownMember, announcedMember} -> + Just $ DEOwnerMemberAnnounced groupInfo unknownMember announcedMember +``` + +### 5. Service.hs — public group link handler + +`deChatLinkReceived`: validates card, calls `apiConnectPlan` (with `resolve=on`), handles per scenario matrix. The link string comes from `MCLGroup.connLink` (`ShortLinkContact`) formatted as URI — passed via command string, parsed inside the handler. For `GLPOk` + `Verified`: joins (with contactId + ownerId), stores `dbOwnerMemberId`, registers as `GRSProposed`. On join error: replies to owner (same pattern as Service.hs:368-370). For `GLPKnown` + `Verified`: re-registration flow. + +### 6. Service.hs — owner member announced handler + +`deOwnerMemberAnnounced`: checks if the announced member's `GroupMemberId` matches `dbOwnerMemberId` of any `GRSProposed` registration. If yes and role is `GROwner`: transition to `GRSPendingApproval`, notify, send for approval. If role < `GROwner`: cancel. + +### 7. Service.hs — deGroupUpdated changes + +For public groups (`groupProfile.publicGroup` present), skip "link in welcome message" check. On profile change, call `apiConnectPlan` with `resolve=on` to get authoritative link data. Compare resolved profile against stored. If different, trigger re-approval. + +### 8. Service.hs — command restrictions and de-registration + +Check `groupProfile.publicGroup` for `/filter`, `/role`. On `/delete` for public groups, call `APILeaveGroup`. Same for owner departure/removal events. + +### 9. Help message update + +``` +To register a channel, share its link with this bot using the "Share via chat" button. +To register a group, invite this bot as admin. +``` + +### 10. Approval message for admins + +Include: group name, description, image, member count, "Registered via link sharing (signed by owner)", publicGroupId. + +### 11. Tests + +**Registration:** +- Share signed card → bot joins, owner announced, pending approval +- Share unsigned card → "must be owner" reply +- Share non-MCLGroup / non-public-group card → "only channels" reply +- Share card with invalid signature → rejection with reason +- Share card, owner never announced → stays GRSProposed +- Share card, owner announced but role < GROwner → cancelled + +**Re-registration (GLPKnown, verified):** +- Same owner re-shares, active → "already listed" +- Same owner re-shares, pending → "already pending" +- Same owner re-shares with changed profile → re-approval +- Different contact, verified owner, previous no longer owner → transfer +- Different contact, verified owner, previous still owner → "registered by another owner" +- Different contact, not owner → rejection + stale ownership check +- Same owner re-shares while GRSProposed, owner still GSMemUnknown → "waiting for owner" + +**Profile updates:** +- XGrpInfo on active public group → re-approval +- XGrpInfo on pending public group → increment approval ID +- XGrpInfo on public group skips link-in-welcome check + +**Owner tracking (via contactId association):** +- Owner role changed → suspension +- Owner leaves → removal, bot leaves +- Owner removed → removal, bot leaves + +**De-registration:** +- `/delete` by owner → removal, bot leaves +- Bot removed → removal +- Admin `/suspend` → suspension, bot stays + +**Commands:** +- `/filter` on public group → disabled +- `/role` on public group → disabled +- `/link` on public group → shows public link diff --git a/plans/2026-04-29-member-profile-sending-channels.md b/plans/2026-04-29-member-profile-sending-channels.md new file mode 100644 index 0000000000..2ee36b676e --- /dev/null +++ b/plans/2026-04-29-member-profile-sending-channels.md @@ -0,0 +1,237 @@ +# Plan: Member Profile Sending in Channels + +## Context + +In channels (relayed groups), subscribers don't know profiles of other subscribers. When subscriber A sends a reaction/message that gets forwarded to subscriber B, B creates an "unknown member" record with a synthesized name. This degrades UX — subscribers see "unknown member" instead of real profiles. + +We can't eagerly send all subscriber profiles to all subscribers (doesn't scale to 100K+ channels). We need on-demand, deduplicated profile delivery: the relay tracks which subscribers have received which sender's profile, and prepends profile info when forwarding a message from a sender the recipient doesn't know. + +## Approach: Vector-tracked profile delivery + +### Core idea + +Each member record on the relay stores a `sent_profile_vector BLOB` — a byte vector where position `i` represents the recipient at `index_in_group = i`. Value 0 = profile not sent, non-zero = sent. + +When the relay forwards a batch (possibly from multiple senders): +1. Collect distinct senders in the batch. Load each sender's `sent_profile_vector`. +2. For each cursor-batch of recipients, partition into two groups: + - **Knows all**: recipient's index is marked as sent in every sender's vector → gets bare batch + - **Needs profiles**: recipient's index is unmarked in at least one sender's vector → gets batch with all sender profiles prepended as `XGrpMemNew` elements +3. Update all senders' vectors to mark recipients who were delivered to. + +When a sender updates their profile (relay receives `XInfo`): clear that sender's `sent_profile_vector`, so the updated profile is re-sent on next forwarded message. + +In steady state, most long-standing subscribers have received all active senders' profiles from previous deliveries. The "knows all" group dominates; the "needs profiles" group consists mainly of newcomers and is small. The partition converges quickly to near-zero redundancy. + +### Why this approach + +**Considered alternatives:** +- **Include profile in every FwdSender**: Wastes bandwidth sending profile on every message. +- **Subscriber requests profile from relay**: Adds latency (round-trip) and new request-response protocol complexity. +- **Separate delivery worker** (using commented-out `DWSMemberProfileUpdate` stubs): Harder to guarantee ordering (profile must arrive before message). +- **Bloom filters / epoch-based**: Same storage complexity as vectors, more complex to implement, probabilistic (false positives). + +**Advantages of prepend-to-batch approach:** +- Profile + forwarded message arrive in a single SMP message (no extra 16KB block overhead) +- SMP guarantees in-order processing within a batch +- No protocol changes — `XGrpMemNew` is already handled by subscribers +- No subscriber-side code changes for receiving + +### Design decisions to discuss + +**1. Bit-level vs byte-level vector** + +Byte-per-position is consistent with `member_relations_vector` but uses 8x more space. For 100K members: byte=100KB/sender, bit=12.5KB/sender. With 1000 active senders: byte=100MB, bit=12.5MB. Byte is simpler; bit is more space-efficient. **Recommend: byte-level for consistency, optimize to bit-level later if needed.** + +**2. Multi-sender batch profile strategy** + +Channels batch tasks from multiple senders into one job (`singleSenderGMId_ = Nothing`). Profile tracking requires knowing which senders' profiles each recipient has seen. Three approaches: + +**Option A — Per-sender precise targeting (rejected)**: For a batch with senders {A, B, C}, construct a separate batch variant for each combination of missing profiles: recipients missing only A get `profile(A) + batch`, those missing A and C get `profile(A) + profile(C) + batch`, etc. This produces up to 2^k batch variants for k senders — a combinatorial explosion that is fundamentally at odds with batching efficiency. Constructing nearly per-recipient blobs is worse than not batching at all. **Rejected.** + +**Option B — All-or-nothing profile sidecar (probably preferable)**: Partition recipients into two groups: those who know ALL senders (get bare batch) and those missing ANY sender profile (get all sender profiles prepended). Only 2 batch variants regardless of sender count. Preserves current multi-sender batching — no changes to `getNextDeliveryTasks`. Some recipients may receive profiles they already know, but XGrpMemNew is idempotent (~200-500 bytes per profile), and this redundancy only occurs at the rare intersection of a multi-sender batch AND a partially-informed recipient. In steady state, long-standing subscribers know all active senders, so the "needs profiles" group shrinks to just newcomers. +- Pros: preserves current batching, smaller diff (no `Store/Delivery.hs` changes), 2 variants only, fast convergence to zero-redundancy steady state +- Cons: slight redundancy for partially-informed recipients in multi-sender batches (rare and transient) + +**Option C — Force single-sender jobs**: Add `sender_group_member_id` filter to `getNextDeliveryTasks` for channels, same as fully connected groups. Each delivery job has exactly one sender, so profile sidecar is always one XGrpMemNew. Clean binary partition with zero redundancy. +- Pros: zero redundant profiles, simplest per-job logic +- Cons: changes delivery task query logic, slightly less batching efficiency (separate jobs per sender), though multi-sender batches are rare anyway + +--- + +## Detailed changes + +The code below assumes Option B (all-or-nothing sidecar). Option C would simplify section 4 (always one sender) and add a query change in `Store/Delivery.hs`. + +### 1. Database migration + +New migration file: `M{date}_sent_profile_vector.hs` + +```sql +ALTER TABLE group_members ADD COLUMN sent_profile_vector BLOB; +``` + +**Files:** +- `src/Simplex/Chat/Store/SQLite/Migrations/M{date}_sent_profile_vector.hs` (new) +- `src/Simplex/Chat/Store/SQLite/Migrations.hs` (register migration) +- `src/Simplex/Chat/Store/Postgres/Migrations/M{date}_sent_profile_vector.hs` (new) +- `src/Simplex/Chat/Store/Postgres/Migrations.hs` (register migration) +- `simplex-chat.cabal` (add module) + +### 2. Sent profile vector operations + +New functions in `src/Simplex/Chat/Store/Groups.hs`: + +```haskell +getSentProfileVector :: DB.Connection -> GroupMemberId -> IO ByteString + +-- Expands vector if needed (same expand-on-write pattern as setRelation in Types/MemberRelations.hs) +markProfilesSentToMembers :: DB.Connection -> GroupMemberId -> [Int64] -> IO () + +clearSentProfileVector :: DB.Connection -> GroupMemberId -> IO () +``` + +Pure helpers: +```haskell +isProfileSentTo :: ByteString -> Int64 -> Bool +isProfileSentTo vec idx + | idx < 0 || fromIntegral idx >= B.length vec = False + | otherwise = B.index vec (fromIntegral idx) /= 0 + +markSentPositions :: [Int64] -> ByteString -> ByteString +``` + +### 3. Profile batch element encoding + +New functions in `src/Simplex/Chat/Messages/Batch.hs`: + +```haskell +-- Prepend an element to an existing binary batch body +-- batchBody format: '=' ( )* +-- Increments count and inserts element at front without parsing/re-encoding existing elements +prependBatchElement :: ByteString -> ByteString -> ByteString + +-- Encode XGrpMemNew as a batch-ready element for a given member +-- Constructs ChatMessage with XGrpMemNew (memberToMemberInfo member) Nothing +encodeMemberProfileElement :: VersionRangeChat -> GroupMember -> ByteString +``` + +Check whether `memberInfo` or similar helper already exists for constructing `MemberInfo` from `GroupMember`. + +### 4. Delivery job worker changes + +**File:** `src/Simplex/Chat/Library/Subscriber.hs` — `processDeliveryJob` / `sendBodyToMembers` + +In the channel path (`useRelays' gInfo`, `DJSGroup {}`): + +**Before the cursor loop**, collect distinct senders from delivery tasks and load their profile data: +```haskell +senderProfiles <- forM (nub senderGMIds) $ \senderGMId -> do + sender <- withStore $ \db -> getGroupMemberById db vr user senderGMId + vec <- withStore' $ \db -> getSentProfileVector db senderGMId + pure (senderGMId, sender, vec) + +let profileElements = map (\(_, sender, _) -> encodeMemberProfileElement vr sender) senderProfiles + extBody = foldl' (flip prependBatchElement) body profileElements +``` + +**In the cursor loop**, partition recipients: +```haskell +sendLoop bucketSize cursorGMId_ = do + mems <- withStore' $ \db -> getGroupMembersByCursor ... + unless (null mems) $ do + if null senderProfiles + then deliver body mems + else do + let knowsAll m = all (\(_, _, vec) -> isProfileSentTo vec (indexInGroup' m)) senderProfiles + (hasAllProfiles, needsProfiles) = partition knowsAll mems + unless (null needsProfiles) $ deliver extBody needsProfiles + unless (null hasAllProfiles) $ deliver body hasAllProfiles + forM_ senderProfiles $ \(senderGMId, _, _) -> + withStore' $ \db -> markProfilesSentToMembers db senderGMId + (map indexInGroup' deliveredMems) + ... +``` + +Only mark vector bits for members who were actually delivered to (those with `readyMemberConn`), not all members in the cursor batch — otherwise members without ready connections get marked as "profile sent" without receiving it. + +### 5. Clear vector on profile update + +**File:** `src/Simplex/Chat/Library/Subscriber.hs` — `xInfoMember` + +After `processMemberProfileUpdate`, if the group uses relays and the user is the relay, clear the sender's vector: + +```haskell +xInfoMember gInfo m p' msg brokerTs = do + void $ processMemberProfileUpdate gInfo m p' (Just (msg, brokerTs)) + when (useRelays' gInfo && isRelay (membership gInfo)) $ + withStore' $ \db -> clearSentProfileVector db (groupMemberId' m) + pure $ memberEventDeliveryScope m +``` + +When the vector is cleared and XInfo is forwarded, the delivery prepends XGrpMemNew before the forwarded XInfo. Recipients process both — XGrpMemNew creates/updates the member record, then XInfo updates it again. Slightly redundant but correct and harmless. + +### 6. Set vector bits when relay announces members at join time + +When a new subscriber joins and the relay sends `XGrpMemNew` for owners/existing announced members, set the corresponding bits in those members' `sent_profile_vector` for the new subscriber's index. The exact location needs to be identified during implementation — look for where the relay processes new member joins and sends XGrpMemNew announcements. + +### 7. Update channel tests + +**File:** `tests/ChatTests/Groups.hs` + +Update `testChannels1RelayDeliver` and related tests: +- After cath sends a reaction, dan and eve should no longer see "forwarded a message from an unknown member, creating unknown member record cath" +- Instead, they receive cath's profile via XGrpMemNew (processed silently before the reaction) +- Test assertions for dan and eve should show the reaction with cath's name + +Add new tests: +- Profile update triggers re-announcement (clear vector → re-send on next message) +- New subscriber joining after a sender has been active gets the profile on first forwarded message +- Multiple senders: each sender's profile is independently tracked + +--- + +## Files to modify + +| File | Change | +|------|--------| +| `src/Simplex/Chat/Store/SQLite/Migrations/M{date}_sent_profile_vector.hs` | New migration | +| `src/Simplex/Chat/Store/SQLite/Migrations.hs` | Register migration | +| `src/Simplex/Chat/Store/Postgres/Migrations/M{date}_sent_profile_vector.hs` | New migration | +| `src/Simplex/Chat/Store/Postgres/Migrations.hs` | Register migration | +| `simplex-chat.cabal` | Add migration module | +| `src/Simplex/Chat/Store/Groups.hs` | Vector CRUD operations | +| `src/Simplex/Chat/Messages/Batch.hs` | `prependBatchElement`, `encodeMemberProfileElement` | +| `src/Simplex/Chat/Library/Subscriber.hs` | Delivery job worker profile logic, xInfoMember vector clear | +| `src/Simplex/Chat/Store/Delivery.hs` | Only if Option C chosen (single-sender jobs) | +| `tests/ChatTests/Groups.hs` | Update channel tests | + +## Subscriber-side impact + +**None required for receiving.** The subscriber already handles: +- `XGrpMemNew` from relay → creates member record with full profile +- `XGrpMsgForward` → finds existing member record +- Mixed batch elements (direct + forwarded) processed in order + +The only subscriber-side change is the test expectations. + +## Verification + +1. **Build**: `cabal build --ghc-options=-O0` +2. **Run channel tests**: `cabal test simplex-chat-test --test-options='-m "channels"'` +3. **Verification scenarios**: + - New subscriber sends reaction → other subscribers receive profile + reaction (no "unknown member") + - Subscriber updates profile → next message re-sends updated profile + - New subscriber joins after sender was active → first forwarded message from that sender includes profile + +## Known considerations + +1. **Vector expansion**: A member with `index_in_group = 100000` causes vector expansion to 100KB. `markSentPositions` handles this via the same expand-on-write pattern as `setRelation` in `Types/MemberRelations.hs`. + +2. **Delivery filtering**: Only mark vector bits for members who were actually delivered to (those with `readyMemberConn`). The `deliver` function filters for ready connections — if `markProfilesSentToMembers` marked all cursor members including those without connections, disconnected members would never receive the profile on reconnection. + +3. **Scope**: Profile tracking applies only to `DJSGroup` scope. Support scope (`DJSMemberSupport`) delivers to moderators who already know members — no profile tracking needed there. + +4. **Sender exclusion**: `getGroupMembersByCursor` already filters out the sender via `singleSenderGMId_` in the WHERE clause, so no self-profile issue arises. + +5. **Race: vector clear vs delivery**: If profile update and message delivery overlap, the delivery sees an empty vector and sends the profile. This is correct — the delivery uses the current (updated) profile, so recipients get the new profile. diff --git a/plans/2026-04-29-relay-management.md b/plans/2026-04-29-relay-management.md new file mode 100644 index 0000000000..a44a9f0b2c --- /dev/null +++ b/plans/2026-04-29-relay-management.md @@ -0,0 +1,415 @@ +# Relay Management Improvements + +## Problem Statement + +Channel owners currently can only add relays during channel creation (`APINewPublicGroup`). Once a channel is created, there is no way to: +1. Add a new relay to an existing channel. +2. Remove a relay from an existing channel. +3. Have relays and subscribers automatically detect and synchronize relay state changes. + +Several TODO markers in the codebase (`[relays]`) confirm these are planned but unimplemented. The `runRelayGroupLinkChecks` function (Commands.hs:4729) is a stub. The LINK event handler (Subscriber.hs:1308-1309) has a TODO for relay deletion detection. No `APIAddGroupRelays` command exists. + +## Solution Summary + +### Add relay to existing channel + +New `APIAddGroupRelays` command that reuses the existing `addRelays` function (Commands.hs:3887, in `processChatCommand`'s `where` block). The `addRelays` flow is asynchronous: after the invitation is sent (RSNew→RSInvited), the relay responds with its relay link (→RSAccepted), and the CON event handler (Subscriber.hs:861-864) calls `setGroupLinkDataAsync` to publish the new relay link. The LINK callback then promotes RSAccepted→RSActive. + +### Remove relay from existing channel + +Use the existing `APIRemoveMembers` command, extended with relay-specific handling. In channels, `APIRemoveMembers` already sends `XGrpMemDel` to all relay members via `sendGroupMessages` (the `memberSendAction` routing ensures the message goes to relays only, which forward it to subscribers). This is the correct approach: broadcasting the removal through *other* relays ensures all subscribers learn about the removal even if the removed relay is malicious and refuses to notify them. Link data synchronization serves as a backup mechanism. + +The extension needed: when removing a relay member, also update its `GroupRelay.relay_status` to `RSInactive`. Currently `APIRemoveMembers` updates `GroupMember` status (via `deleteOrUpdateMemberRecordIO`) and calls `updatePublicGroupData` (which updates link data), but does not touch the `GroupRelay` record. + +### State synchronization + +Three actors synchronize via the group link data on the SMP server: + +- **Owner**: publishes the authoritative relay list in link data via `setGroupLinkData`. The `getConnectedGroupRelays` function (which filters by `member_status = GSMemConnected AND relay_status IN (RSAccepted, RSActive)`) determines which relays appear in link data. +- **Relay**: `runRelayGroupLinkChecks` (implement the stub) periodically fetches group link data to confirm its own link is present. If absent → self-cleanup. +- **Subscriber**: when opening a channel, the UI already calls `APIGetUpdatedGroupLinkData` (Commands.hs:1777) which fetches link data from the SMP server. This handler will be extended to also synchronize relay state: connect to newly discovered relays, disconnect from removed relays. + +--- + +## Detailed Technical Design + +### 1. Relay Deactivation on Member Removal + +**File**: `src/Simplex/Chat/Library/Internal.hs` (lines 1804-1821) + +Two member-removal primitives exist: `deleteOrUpdateMemberRecordIO` (IO, line 1808) and `updateMemberRecordDeleted` (CM, line 1816). Both run in DB context. Relay deactivation belongs inside these functions so it runs in the same transaction as the member status change. + +**New helper** in Internal.hs: + +```haskell +deactivateRelayIfNeeded :: DB.Connection -> GroupMember -> IO () +deactivateRelayIfNeeded db m = + when (isRelay m) $ do + relay_ <- runExceptT $ getGroupRelayByGMId db (groupMemberId' m) + forM_ relay_ $ \relay -> void $ updateRelayStatus db relay RSInactive +``` + +**Extend `deleteOrUpdateMemberRecordIO`** (line 1808): + +```haskell +deleteOrUpdateMemberRecordIO db user@User {userId} gInfo m = do + (gInfo', m') <- deleteSupportChatIfExists db user gInfo m + checkGroupMemberHasItems db user m' >>= \case + Just _ -> updateGroupMemberStatus db userId m' GSMemRemoved + Nothing -> deleteGroupMember db user m' + deactivateRelayIfNeeded db m + pure gInfo' +``` + +**Extend `updateMemberRecordDeleted`** (line 1816): + +```haskell +updateMemberRecordDeleted user@User {userId} gInfo m newStatus = + withStore' $ \db -> do + (gInfo', m') <- deleteSupportChatIfExists db user gInfo m + updateGroupMemberStatus db userId m' newStatus + deactivateRelayIfNeeded db m + pure gInfo' +``` + +This covers all four call sites: +- `delMember` in `deleteMemsSend` (Commands.hs:2896) — owner removing relay via `APIRemoveMembers` +- `deleteOrUpdateMemberRecord` in `xGrpMemDel` (Subscriber.hs:3123) — receiving relay deletion notification +- `updateMemberRecordDeleted` in `xGrpMemDel` (Subscriber.hs:3121) — relay deletion with forwarding +- `updateMemberRecordDeleted` in `xGrpLeave` (Subscriber.hs:3168) — relay leaves voluntarily + +For subscribers who have no `GroupRelay` records, `getGroupRelayByGMId` returns `Left`, `forM_` on `Left` is a no-op — safe. + +**Cleanup**: remove the now-redundant separate relay deactivation in `xGrpLeave` (Subscriber.hs:3169-3172): + +```haskell +-- Before: +gInfo' <- updateMemberRecordDeleted user gInfo m GSMemLeft +when (isRelay m) $ + withStore' $ \db -> do + relay_ <- runExceptT $ getGroupRelayByGMId db (groupMemberId' m) + forM_ relay_ $ \relay -> void $ updateRelayStatus db relay RSInactive +gInfo'' <- updatePublicGroupData user gInfo' + +-- After: +gInfo' <- updateMemberRecordDeleted user gInfo m GSMemLeft +gInfo'' <- updatePublicGroupData user gInfo' +``` + +**`APIRemoveMembers` requires no changes** — `delMember` (line 2891) already calls `deleteOrUpdateMemberRecordIO` which now handles relay deactivation internally. The `getConnectedGroupRelays` query filters by both `member_status = GSMemConnected` and `relay_status IN (RSAccepted, RSActive)`, so the removed relay is excluded from link data when `updatePublicGroupData` runs (line 2828-2829). + +**iOS UI**: The remove button is currently hidden on the relay member info page by an explicit guard in `adminDestructiveSection` (GroupMemberInfoView.swift:646: `mem.memberRole != .relay`). Changes needed: + +1. **Remove the relay guard** — change the condition to allow relay members to be removed. The `canBeRemoved()` permission check (ChatTypes.swift:2868) already validates that the user has sufficient role. + +2. **Relay-specific button text** — the `removeMemberButton` (line 708) currently shows `"Remove subscriber"` for channels (`groupInfo.useRelays`). Add a relay branch: when `mem.memberRole == .relay`, show `"Remove relay"` instead. + +3. **Relay-specific alert text** — `showRemoveMemberAlert` (GroupChatInfoView.swift:926) currently shows `"Remove subscriber?"` / `"Subscriber will be removed from channel"` for channels. Add a relay branch: `"Remove relay?"` / `"Relay will be removed from channel"`. + +4. **Last active relay warning** — when removing a relay, check if it's the last active relay (count relay members with `memberCurrent` status in `chatModel.groupMembers`). If so, show a warning: `"This is the last active relay. Removing it will prevent message delivery to subscribers."` The count is available client-side from `chatModel.groupMembers.filter { $0.wrapped.memberRole == .relay && $0.wrapped.memberCurrent }`. + +No new API command needed for removal — the existing `apiRemoveMembers` is used. + +### 2. New `APIAddGroupRelays` Command + +**File**: `src/Simplex/Chat/Controller.hs` + +```haskell +-- New command +| APIAddGroupRelays GroupId (NonEmpty Int64) -- group ID, chat_relay_ids + +-- New responses +| CRGroupRelaysAdded { user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay] } +| CRGroupRelaysAddFailed { user :: User, addRelayResults :: [AddRelayResult] } +``` + +**File**: `src/Simplex/Chat/Library/Commands.hs` + +New handler: + +``` +APIAddGroupRelays groupId relayIds -> withUser $ \user -> withGroupLock "addGroupRelays" groupId $ do + -- 1. Validate: user is owner, group uses relays + gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + assertUserGroupRole gInfo GROwner + unless (useRelays' gInfo) $ throwCmdError "group does not use relays" + + -- 2. Get group link (needed for relay invitation) + gLink <- withFastStore $ \db -> getGroupLink db user gInfo + sLnk <- case connShortLink' (connLinkContact gLink) of + Just sl -> pure sl + Nothing -> throwChatError $ CEException "group link has no short link" + + -- 3. Load requested relay configs + relays <- withFastStore $ \db -> mapM (getChatRelayById db user) (L.toList relayIds) + + -- 4. Reuse existing addRelays function (Commands.hs:3887) + results <- addRelays user gInfo sLnk relays + + -- 5. Check results + case partitionEithers (map snd results) of + ([], _) -> do + -- Relay connection is asynchronous: invitation sent (RSNew→RSInvited). + -- When relay responds (RSAccepted) and connects (CON at Subscriber.hs:861-864), + -- setGroupLinkDataAsync is called automatically to add the relay link. + -- The LINK callback then promotes RSAccepted→RSActive. + relays' <- withFastStore $ \db -> liftIO $ getGroupRelays db gInfo + pure $ CRGroupRelaysAdded user gInfo gLink relays' + _ -> do + let toRelayResult (r, Left e) = AddRelayResult r (Just e) + toRelayResult (r, Right _) = AddRelayResult r Nothing + pure $ CRGroupRelaysAddFailed user (map toRelayResult results) +``` + +Key points: +- Uses `withGroupLock` to prevent concurrent relay modifications. +- Reuses `addRelays` unchanged — it handles the full invitation flow (create relay member, create GroupRelay record, send `XGrpRelayInv`, update status RSNew→RSInvited). +- No synchronous `setGroupLinkData` call needed: the CON event handler calls `setGroupLinkDataAsync` when the relay connects. + +### 3. Extend `APIGetUpdatedGroupLinkData` for Subscriber Relay Sync + +**File**: `src/Simplex/Chat/Library/Commands.hs` (lines 1777-1787) + +Currently this handler fetches link data from the SMP server and updates group profile and member count. It is called by the iOS UI when a non-owner subscriber opens a channel (ChatView.swift:750). The `ConnLinkData` it receives already contains the relay list in `UserContactData.relays`. + +Extend the handler to also synchronize relay state: + +``` +APIGetUpdatedGroupLinkData groupId -> withUser $ \user -> do + gInfo@GroupInfo {groupProfile = p} <- withFastStore $ \db -> getGroupInfo db vr user groupId + case p of + GroupProfile {publicGroup = Just PublicGroupProfile {groupLink = sLnk}} | useRelays' gInfo -> do + (_, cData@(ContactLinkData _ UserContactData {relays = currentRelayLinks})) <- + getShortLinkConnReq nm user sLnk + groupSLinkData_ <- liftIO $ decodeLinkUserData cData + gInfo' <- case groupSLinkData_ of + Just sLinkData -> fst <$> updateGroupFromLinkData user gInfo sLinkData + _ -> pure gInfo + -- Sync relay state for non-owner subscribers + when (memberRole' (membership gInfo) /= GROwner) $ + syncSubscriberRelays nm user gInfo' currentRelayLinks + pure $ CRGroupInfo user gInfo' + _ -> throwCmdError "group link data not available" +``` + +**Parameterize `connectToRelay`** — move from `APIConnectPreparedGroup`'s `where` block to `processChatCommand`'s `where` block so both `APIConnectPreparedGroup` and subscriber sync can use it. The captured closure variables become explicit parameters or are derived internally: + +``` +-- In processChatCommand's where block (for connectViaContact access). +-- connectViaContact ignores incognito param for relay groups (Commands.hs:3545-3546), +-- using incognitoMembershipProfile gInfo instead. +connectToRelay :: User -> GroupInfo -> ShortLinkContact -> CM (ShortLinkContact, GroupMember, Either ChatError ()) +connectToRelay user gInfo relayLink = do + vr <- chatVersionRange + gVar <- asks random + relayMember <- withFastStore $ \db -> getCreateRelayForMember db vr gVar user gInfo relayLink + r <- tryAllErrors $ do + (fd@FixedLinkData {rootKey = relayKey, linkEntityId}, cData) <- + getShortLinkConnReq nm user relayLink + relayLinkData_ <- liftIO $ decodeLinkUserData cData + case (relayLinkData_, linkEntityId) of + (Just RelayShortLinkData {relayProfile = p}, Just entityId) -> + withFastStore $ \db -> updateRelayMemberData db user relayMember (MemberId entityId) (MemberKey relayKey) p + _ -> throwChatError $ CEException "relay link: no relay link data or entity id" + let cReq = linkConnReq fd + relayLinkToConnect = CCLink cReq (Just relayLink) + void $ connectViaContact user (Just $ PCEGroup gInfo relayMember) False relayLinkToConnect Nothing Nothing + relayMember' <- withFastStore $ \db -> getGroupMember db vr user (groupId' gInfo) (groupMemberId' relayMember) + pure (relayLink, relayMember', r) +``` + +`getCreateRelayForMember` stays outside `tryAllErrors` — the member must be available for re-read even on failure (for `RelayConnectionResult` reporting). `APIConnectPreparedGroup` calls `mapConcurrently (connectToRelay user gInfo') relays` as before. + +**New function** `syncSubscriberRelays` in `processChatCommand`'s scope (reuses `connectToRelay`): + +``` +syncSubscriberRelays :: NetworkRequestMode -> User -> GroupInfo -> [ShortLinkContact] -> CM () +syncSubscriberRelays nm user gInfo currentRelayLinks = tryAllErrors $ do + vr <- chatVersionRange + -- Get local relay members (all members with GRRelay role, regardless of status) + localRelayMembers <- withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo + -- GroupMember.relayLink :: Maybe ShortLinkContact (Types.hs:1041) + -- Set by getCreateRelayForMember (Store/Groups.hs:1392) when subscriber connects to a relay. + let activeRelayMembers = filter memberCurrent localRelayMembers + localRelayLinks = mapMaybe relayLink activeRelayMembers + + -- Discover new relays (in link data but not among active local relay members) + let newRelayLinks = filter (`notElem` localRelayLinks) currentRelayLinks + forM_ newRelayLinks $ \rlnk -> tryAllErrors $ + void $ connectToRelay user gInfo rlnk + + -- Discover removed relays (active local relay member whose link is absent from link data) + forM_ activeRelayMembers $ \m -> + case relayLink m of + Just rlnk | rlnk `notElem` currentRelayLinks -> + tryAllErrors $ do + deleteMemberConnection m + void $ updateMemberRecordDeleted user gInfo m GSMemRemoved + _ -> pure () +``` + +**Note on `getCreateRelayForMember` idempotency**: This function queries `WHERE m.relay_link = ?` without filtering by member status (Store/Groups.hs:1379). If a relay was previously removed (GSMemRemoved) and is later re-added by the owner, `getCreateRelayForMember` will return the old removed member. During implementation, verify whether the member status needs to be reset before reconnecting, or whether `connectViaContact` handles this correctly. + +### 4. LINK Event Handler — Detect Relay Removal (Owner) + +**File**: `src/Simplex/Chat/Library/Subscriber.hs` (lines 1308-1317) + +Replace the TODO with relay removal detection. The LINK callback fires when this owner updates link data (via `setGroupLinkData` / `setConnShortLink`). Currently multi-owner channels are not supported, so this only fires after the same owner's own actions (add/remove relay, profile update). When multi-owner support is added, another owner's link data update on the SMP server would need a separate mechanism (e.g., periodic link data fetch or subscription) for this owner to learn about it — the LINK callback only fires in response to this client's own `setConnShortLink` calls. + +```haskell +updateRelay :: DB.Connection -> GroupRelay -> ([GroupRelay], Bool) -> IO ([GroupRelay], Bool) +updateRelay db relay@GroupRelay {relayLink, relayStatus} (acc, changed) = + case relayLink of + Just rLink + | rLink `elem` relayLinks && relayStatus == RSAccepted -> do + -- Relay link present in link data, promote to active + relay' <- updateRelayStatus db relay RSActive + pure (relay' : acc, True) + | rLink `elem` relayLinks -> pure (relay : acc, changed) + | relayStatus `elem` [RSAccepted, RSActive, RSInactive] -> do + -- Relay link ABSENT from link data — set to inactive. + -- TODO [multi-owner] When multi-owner channels are supported, another owner removing + -- a relay updates link data on the SMP server, but this owner won't receive a LINK + -- callback for it (LINK only fires in response to own setConnShortLink calls). + -- A separate mechanism will be needed for cross-owner relay state synchronization. + relay' <- updateRelayStatus db relay RSInactive + pure (relay' : acc, True) + _ -> pure (relay : acc, changed) +``` + +After the same owner's `APIRemoveMembers` call, the relay is already `RSInactive` before `updatePublicGroupData` triggers the LINK callback. The guard matches `RSInactive` but `updateRelayStatus` is idempotent (RSInactive→RSInactive is a no-op write). + +### 5. Relay Self-Check (`runRelayGroupLinkChecks`) + +**File**: `src/Simplex/Chat/Library/Commands.hs` (lines 4729-4735) + +Implement the stub. The existing `startRelayChecks` (Commands.hs:225-233) already launches `runRelayGroupLinkChecks` as an async task via `relayGroupLinkChecksAsync`. The stub currently does `pure ()` and exits immediately. Replace with a periodic loop following the `cleanupManager` pattern (Commands.hs:4643): + +``` +runRelayGroupLinkChecks :: User -> CM () +runRelayGroupLinkChecks user = do + initialDelay <- asks (initialCleanupManagerDelay . config) + liftIO $ threadDelay' initialDelay + interval <- asks (cleanupManagerInterval . config) -- or a dedicated config field + forever $ do + flip catchAllErrors eToView $ do + lift waitChatStartedAndActivated + checkRelayGroups + liftIO $ threadDelay' $ diffToMicroseconds interval + where + checkRelayGroups = do + vr <- chatVersionRange + -- Get all groups where this client is a relay (relay_own_status is set and not RSInactive) + relayGroups <- withFastStore' $ \db -> getRelayOwnGroups db vr user + forM_ relayGroups $ \gInfo -> tryAllErrors $ do + case publicGroup (groupProfile gInfo) of + Just PublicGroupProfile {groupLink = sLnk} -> do + -- getShortLinkConnReq' returns (FixedLinkData, ConnLinkData m). + -- ConnLinkData 'CMContact = ContactLinkData VersionRangeSMPA UserContactData + -- (NOT UserContactLinkData which is for the LINK event's auData) + (_, ContactLinkData _ UserContactData {relays = relayLinks}) <- + getShortLinkConnReq' NRMBackground user sLnk + -- Check if our own relay link is present + gLink_ <- withFastStore' $ \db -> runExceptT $ getGroupLink db user gInfo + case gLink_ of + Right GroupLink {connLinkContact = CCLink _ (Just ourLink)} -> + if ourLink `elem` relayLinks + then do + -- Our link is present — promote to RSActive if still RSAccepted + gInfo' <- withFastStore' $ \db -> updateRelayOwnStatusFromTo db gInfo RSAccepted RSActive + when (relayOwnStatus gInfo' /= relayOwnStatus gInfo) $ + toView $ CEvtGroupRelayUpdated user gInfo' (membership gInfo') + else do + -- Our link is ABSENT — we have been removed + withFastStore' $ \db -> updateRelayOwnStatus_ db gInfo RSInactive + -- Per RFC: relay should forward "relay is deleted" notification to + -- connected members, then clean up. The x.grp.mem.del from owner + -- may also arrive and trigger cleanup independently. + _ -> pure () + _ -> pure () +``` + +**New store function** in `Store/Groups.hs`: + +```haskell +getRelayOwnGroups :: DB.Connection -> VersionRangeChat -> User -> IO [GroupInfo] +-- SELECT groups WHERE relay_own_status IS NOT NULL AND relay_own_status != 'inactive' +``` + +--- + +## State Synchronization Summary + +``` + SMP Server (group link data) + ┌──────────────────────────────┐ + │ UserContactData { │ + │ relays: [relay1, relay2] │ + │ } │ + └──────────┬───────────────────┘ + │ + ┌───────────────┼───────────────┐ + │ writes │ reads │ reads + ▼ ▼ ▼ + Owner Relay (self) Subscriber + setGroupLinkData runRelayGroup syncSubscriber + (via updatePublic LinkChecks Relays (in + GroupData) APIGetUpdated + GroupLinkData) + Triggers: Triggers: Triggers: + - Add relay - Periodic check - Opening channel + - Remove member (existing UI flow) + - Profile update +``` + +**Owner writes** → SMP server is updated → **Relays and Subscribers read** → discover changes → adjust local state. + +**Key design principle**: The `XGrpMemDel` message broadcast through other relays is the primary notification mechanism for relay removal. Subscribers receive it promptly via their connected relays. Link data synchronization via `APIGetUpdatedGroupLinkData` is the backup mechanism — it catches cases where the `XGrpMemDel` was missed (subscriber offline, relay connection issues) and handles new relay discovery. + +--- + +## Edge Cases and Failure Recovery + +1. **Add relay fails (network)**: `addRelays` handles temporary errors. The relay remains in RSInvited; owner can retry or the relay will process the pending invitation when it comes online. + +2. **Removed relay is malicious / refuses to notify subscribers**: Not a problem. `APIRemoveMembers` sends `XGrpMemDel` to all relay members. Other (non-malicious) relays forward it to subscribers. Subscribers learn about the removal regardless of the removed relay's behavior. + +3. **Remove relay, all relays offline**: `XGrpMemDel` is queued for delivery. Link data is still updated. Subscribers will discover the change via `APIGetUpdatedGroupLinkData` next time they open the channel. + +4. **Owner removes last relay**: Subscribers lose message delivery. Owner must add a new relay. Subscribers will discover the new relay via `syncSubscriberRelays` when they next open the channel. + +5. **Relay goes offline permanently**: Owner removes it via `APIRemoveMembers`. New subscribers won't see it in link data. Existing subscribers with connections to this relay will experience connection failures. On next channel open, `syncSubscriberRelays` discovers the relay link is gone and marks it removed locally. + +6. **Subscriber discovers new relay via link data**: `syncSubscriberRelays` calls `connectToRelay` (same function used by `APIConnectPreparedGroup`). + +--- + +## Implementation Order + +1. **Relay deactivation in member-removal primitives** — add `deactivateRelayIfNeeded` helper to `deleteOrUpdateMemberRecordIO` and `updateMemberRecordDeleted` in Internal.hs; remove redundant code from `xGrpLeave`. +2. **LINK handler relay-removal detection** — implement the TODO in Subscriber.hs to detect absent relay links. +3. **`APIAddGroupRelays`** — new command, reuses `addRelays`. +4. **`runRelayGroupLinkChecks`** — relay self-check implementation. +5. **Extend `APIGetUpdatedGroupLinkData`** — add `syncSubscriberRelays` for subscriber relay synchronization. +6. **iOS UI** — ChannelRelaysView add/remove buttons, AddGroupRelayView sheet, API functions. + +## Files Changed (Backend) + +| File | Change | +|------|--------| +| `src/Simplex/Chat/Controller.hs` | Add `APIAddGroupRelays` command; add `CRGroupRelaysAdded`, `CRGroupRelaysAddFailed` responses | +| `src/Simplex/Chat/Library/Internal.hs` | Add `deactivateRelayIfNeeded` helper; extend `deleteOrUpdateMemberRecordIO` and `updateMemberRecordDeleted` to call it | +| `src/Simplex/Chat/Library/Commands.hs` | Parameterize and move `connectToRelay` to `processChatCommand` scope; implement `APIAddGroupRelays` handler; implement `runRelayGroupLinkChecks`; extend `APIGetUpdatedGroupLinkData`; add `syncSubscriberRelays` (all in `processChatCommand` scope for `connectViaContact` access) | +| `src/Simplex/Chat/Library/Subscriber.hs` | Fix LINK handler relay removal detection; remove redundant relay deactivation from `xGrpLeave` | +| `src/Simplex/Chat/Store/Groups.hs` | Add `getRelayOwnGroups` | + +## Files Changed (iOS) + +| File | Change | +|------|--------| +| `apps/ios/Shared/Model/AppAPITypes.swift` | Add `APIAddGroupRelays` command, `CRGroupRelaysAdded`/`CRGroupRelaysAddFailed` responses | +| `apps/ios/Shared/Model/SimpleXAPI.swift` | Add `apiAddGroupRelays` function | +| `apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift` | Remove `.relay` guard from `adminDestructiveSection` (line 646); add relay-specific button/alert text; add last-active-relay warning | +| `apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift` | Add relay branch to `showRemoveMemberAlert` text | +| `apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift` | Add relay button | +| `apps/ios/Shared/Views/Chat/Group/AddGroupRelayView.swift` | NEW: relay selection sheet | diff --git a/plans/2026-04-29-relay-request-retry-limit.md b/plans/2026-04-29-relay-request-retry-limit.md new file mode 100644 index 0000000000..34d0f4c9f0 --- /dev/null +++ b/plans/2026-04-29-relay-request-retry-limit.md @@ -0,0 +1,203 @@ +# Plan: Relay Request Worker Retry Limit + +## Context + +The relay request worker (`runRelayRequestWorker`) processes channel setup requests sequentially using a single worker (`relayRequestWorkerKey = 1`). When a request requires network calls to an unreachable server (e.g., fetching group link data via `getShortLinkConnReq'`), the worker retries indefinitely via `withRetryInterval` + `retryTmpError` — temp/host errors call `loop` with no limit. This blocks all subsequent relay requests from processing. + +This is an attack vector: a channel owner can create a channel link on a server unreachable by the relay, causing the relay request worker to retry forever and blocking all other channel setup requests. + +## Approach + +Follow the XFTP worker retry pattern (`runXFTPDelWorker` in `simplexmq/src/Simplex/FileTransfer/Agent.hs:667`): + +1. **Track retries and delay in DB**: Add `relay_request_retries` and `relay_request_delay` columns to the `groups` table +2. **Order by retries**: Query for next work item ordered by `relay_request_retries ASC, created_at ASC` — items with fewer retries are processed first, stuck items get pushed to the back +3. **Limit consecutive retries**: Replace `withRetryInterval` with `withRetryIntervalCount`, limiting to a small number of consecutive retries per pickup cycle (3, matching XFTP's `xftpConsecutiveRetries`). After the limit, the worker yields and picks the next item. +4. **Store delay for resumption**: On each retry, store the current backoff delay in DB. On next pickup, resume backoff from the stored delay (XFTP pattern: `ri {initialInterval = d, increaseAfter = 0}`) +5. **Expire old requests**: On temp error, before retrying, check if the request is older than 1 day and has 10+ retries — if so, mark as failed instead of retrying. Both conditions must hold — a request that's old but has few retries may just have been delayed, while a request with many retries that's recent is still being actively worked on. + +### How this neutralizes the attack + +- Attacker's request gets picked up, retried 3 times with backoff (~15s total), then yielded +- Worker picks the next item by retry count — legitimate requests (retries=0) go first +- Attacker's request accumulates retries, always processed last +- After 1 day and 10+ retries, the request is marked failed and permanently excluded + +--- + +## Detailed changes + +### 1. Database migration + +New migration: `M20260429_relay_request_retries.hs` + +```sql +ALTER TABLE groups ADD COLUMN relay_request_retries INTEGER NOT NULL DEFAULT 0; +ALTER TABLE groups ADD COLUMN relay_request_delay INTEGER; +``` + +**Files:** +- `src/Simplex/Chat/Store/SQLite/Migrations/M20260429_relay_request_retries.hs` (new) +- `src/Simplex/Chat/Store/SQLite/Migrations.hs` (register) +- `src/Simplex/Chat/Store/Postgres/Migrations/M20260429_relay_request_retries.hs` (new) +- `src/Simplex/Chat/Store/Postgres/Migrations.hs` (register) +- `simplex-chat.cabal` (add modules) + +### 2. Extend RelayRequestData + +**File:** `src/Simplex/Chat/Types.hs` + +```haskell +data RelayRequestData = RelayRequestData + { relayInvId :: InvitationId, + reqGroupLink :: ShortLinkContact, + reqChatVRange :: VersionRangeChat, + relayRequestDelay :: Maybe Int64, + relayRequestRetries :: Int, + relayRequestCreatedAt :: UTCTime + } +``` + +- `relayRequestDelay`: resume backoff from stored position (XFTP pattern) +- `relayRequestRetries`: current retry count, used with `relayRequestCreatedAt` to decide expiry in `retryTmpError` +- `relayRequestCreatedAt`: group creation time, used for the 1-day expiry check + +### 3. Update store functions + +**File:** `src/Simplex/Chat/Store/RelayRequests.hs` + +**`getNextPendingRelayRequest`** — two changes: +- Order by `relay_request_retries ASC, created_at ASC` instead of `group_id ASC` +- SELECT and return `relay_request_delay`, `relay_request_retries`, `created_at` in the data query + +```haskell +getNextPendingRelayRequest db = + getWorkItem "relay request" getNextRequestGroupId getRelayRequestData (markRelayRequestFailed db) + where + getNextRequestGroupId = + maybeFirstRow fromOnly $ + DB.query db + [sql| + SELECT group_id FROM groups + WHERE relay_own_status = ? + AND relay_request_failed = 0 + AND relay_request_err_reason IS NULL + ORDER BY relay_request_retries ASC, created_at ASC + LIMIT 1 + |] + (Only RSInvited) + getRelayRequestData groupId = + firstRow' toRelayRequestData (SEGroupNotFound groupId) $ + DB.query db + [sql| + SELECT relay_request_inv_id, relay_request_group_link, + relay_request_peer_chat_min_version, relay_request_peer_chat_max_version, + relay_request_delay, relay_request_retries, created_at + FROM groups WHERE group_id = ? + |] + (Only groupId) + where + toRelayRequestData (Just relayInvId, Just reqGroupLink, Just minV, Just maxV, relayRequestDelay, relayRequestRetries, relayRequestCreatedAt) = + Right (groupId, RelayRequestData {relayInvId, reqGroupLink, reqChatVRange = fromMaybe (versionToRange maxV) $ safeVersionRange minV maxV, relayRequestDelay, relayRequestRetries, relayRequestCreatedAt}) + toRelayRequestData _ = Left $ SEInternalError "missing relay request data" +``` + +**New function: `updateRelayRequestRetries`**: + +```haskell +updateRelayRequestRetries :: DB.Connection -> GroupId -> Int64 -> IO () +updateRelayRequestRetries db groupId delay = do + currentTs <- getCurrentTime + DB.execute db + "UPDATE groups SET relay_request_retries = relay_request_retries + 1, relay_request_delay = ?, updated_at = ? WHERE group_id = ?" + (delay, currentTs, groupId) +``` + +Export `updateRelayRequestRetries` and `markRelayRequestFailed` from module (the latter is currently internal, used only as a callback in `getWorkItem`). + +### 4. Worker changes + +**File:** `src/Simplex/Chat/Library/Subscriber.hs` + +**Import change**: Add `withRetryIntervalCount` to the import from `Simplex.Messaging.Agent.RetryInterval`. + +**Replace `withRetryInterval` with limited retry** in `runRelayRequestOperation`: + +```haskell +runRelayRequestOperation vr user uclId = + withWork_ a doWork (withStore' getNextPendingRelayRequest) $ + \(groupId, rrd@RelayRequestData {relayRequestDelay}) -> do + ri <- asks $ reconnectInterval . agentConfig . config + let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) relayRequestDelay + withRetryIntervalLimit ri' $ \delay loop -> do + liftIO $ waitWhileSuspended a + liftIO $ waitForUserNetwork a + processRelayRequest groupId rrd `catchAllErrors` retryTmpError loop groupId rrd delay + where + maxConsecutiveRetries :: Int + maxConsecutiveRetries = 3 + withRetryIntervalLimit :: RetryInterval -> (Int64 -> CM () -> CM ()) -> CM () + withRetryIntervalLimit ri action = + withRetryIntervalCount ri $ \n delay loop -> + when (n < maxConsecutiveRetries) $ action delay loop + retryTmpError :: CM () -> GroupId -> RelayRequestData -> Int64 -> ChatError -> CM () + retryTmpError loop groupId RelayRequestData {relayRequestRetries, relayRequestCreatedAt} delay = \case + ChatErrorAgent {agentError} | temporaryOrHostError agentError -> do + currentTs <- liftIO getCurrentTime + if relayRequestRetries >= 10 && diffUTCTime currentTs relayRequestCreatedAt > nominalDay + then withStore' $ \db -> markRelayRequestFailed db groupId + else do + withStore' $ \db -> updateRelayRequestRetries db groupId delay + loop + e -> do + withStore' $ \db -> setRelayRequestErr db groupId (tshow e) + eToView e +``` + +Key changes from current code: +- `withRetryInterval` → `withRetryIntervalCount` wrapped in local `withRetryIntervalLimit` +- Resume from stored delay via `ri'` (XFTP pattern) +- `retryTmpError` receives the full `RelayRequestData` record and destructures the fields it needs +- On temp error: checks if request is older than 1 day with 10+ retries — if so, marks as failed instead of retrying; otherwise increments retries and calls `loop` +- After `maxConsecutiveRetries` (3), the `when` guard exits, worker picks next item + +--- + +## Files to modify + +| File | Change | +|------|--------| +| `src/Simplex/Chat/Store/SQLite/Migrations/M20260429_relay_request_retries.hs` | New migration | +| `src/Simplex/Chat/Store/SQLite/Migrations.hs` | Register migration | +| `src/Simplex/Chat/Store/Postgres/Migrations/M20260429_relay_request_retries.hs` | New migration | +| `src/Simplex/Chat/Store/Postgres/Migrations.hs` | Register migration | +| `simplex-chat.cabal` | Add migration modules | +| `src/Simplex/Chat/Types.hs` | Add `relayRequestDelay`, `relayRequestRetries`, `relayRequestCreatedAt` to `RelayRequestData` | +| `src/Simplex/Chat/Store/RelayRequests.hs` | Retry ordering, `updateRelayRequestRetries` | +| `src/Simplex/Chat/Library/Subscriber.hs` | Limited retry with delay storage, expiry check in `retryTmpError` | + +## Verification + +1. **Build**: `cabal build --ghc-options=-O0` +2. **Run relay tests**: `cabal test simplex-chat-test --test-options='-m "relay"'` +3. **Scenarios**: + - Request to unreachable server: retried 3 times per cycle, pushed to back of queue, marked failed after 1 day and 10+ retries + - Request to reachable server: succeeds on first attempt, unaffected by changes + - Multiple pending requests: stuck request doesn't block others — items with fewer retries processed first + - App restart with expired pending requests: worker starts, picks up expired request, attempts it — if it succeeds (server now reachable), completes normally; if it fails, `retryTmpError` marks it failed + +## Known considerations + +1. **Single stuck item re-pickup**: If only one request is pending and it's stuck, the worker picks it up repeatedly (3 retries each cycle, immediate re-pickup). This is acceptable — backoff grows via stored delay, and the request is marked failed after 1 day and 10+ retries. The main protection is that other requests aren't blocked. + +2. **`hasPendingRelayRequests` unchanged**: Expired requests still match the `hasPendingRelayRequests` query at startup, so the worker starts. It picks them up, attempts processing — if the server became reachable, the request succeeds normally. If it fails, `retryTmpError` checks the expiry condition and marks it failed. This is strictly better than filtering at query time: expired items get one last chance. + +3. **Delay resumption across pickups**: Stored delay resumes backoff at the last level (XFTP pattern). After many cycles, delay reaches `maxInterval` and stays there. This means retry frequency stabilizes at a low rate for stuck items. + +4. **Permanent errors unchanged**: Non-temp errors (validation, logic) still call `setRelayRequestErr` immediately, permanently excluding the item. The retry mechanism only affects `temporaryOrHostError`. + +5. **`withWork_` re-signals work**: After the action returns (hitting max consecutive retries), `withWork_` has already called `hasWork` (re-signaling the doWork TMVar). The outer `forever` loop immediately proceeds to the next iteration. This is the desired behavior — the worker processes all pending items before waiting. + +6. **`retries` count is from pickup time**: The `relayRequestRetries` value in `retryTmpError` is the count loaded when the item was picked up. Within a single pickup cycle (up to 3 consecutive retries), `updateRelayRequestRetries` increments the DB count but the local value stays the same. The expiry check uses the pickup-time count, which is at most 3 behind the DB. This is acceptable — the threshold (10) has margin. + +7. **Migration column defaults**: `relay_request_retries NOT NULL DEFAULT 0` ensures existing pending requests start with 0 retries. `relay_request_delay` is nullable (NULL = use default reconnectInterval), matching the `Maybe Int64` field. diff --git a/plans/2026-05-01-support-bot-list-api-pagination.md b/plans/2026-05-01-support-bot-list-api-pagination.md new file mode 100644 index 0000000000..44cfde7971 --- /dev/null +++ b/plans/2026-05-01-support-bot-list-api-pagination.md @@ -0,0 +1,377 @@ +# Plan: Fix support-bot crash on large databases — use pagination and direct lookup + +## Context + +The simplex-support-bot crashes during startup against large production +databases: + +``` +[2026-04-30T15:52:53.498Z] Grok contact from state: ID=142676 +[2026-04-30T15:52:53.498Z] Resolving team group... +:0 +[Error: Unknown failure] +``` + +The crash happens inside `chat.apiListGroups(mainUser.userId)` at +`apps/simplex-support-bot/src/index.ts:215`. The native binding marshals the +Haskell core's response to a JS string at +`packages/simplex-chat-nodejs/cpp/simplex.cc:255` (`chat_send_cmd`) → +`HandleCResult` (line 157) → `Napi::String::New` in `OnOK`. When the response +exceeds V8's max string length (~512 MB on 64-bit), N-API string allocation +fails. The literal string `"Unknown failure"` does **not** appear anywhere in +this repo — confirmed by full-tree search — so the message originates from V8 +or N-API internals rather than the binding's own error path (which would say +`chat_send_cmd failed`). Hypothesis: oversized string allocation throws a JS +exception that propagates up unannotated. + +Two distinct misuse patterns drive the payload size: + +**A. List-then-find by ID** (most call sites). The bot pulls every contact / +every group with `apiListContacts` / `apiListGroups`, then calls `find(...)` +to locate one record by a known ID. This is gratuitous — there is already +`apiGetChat(chatType, chatId, count=0)` (`packages/simplex-chat-nodejs/src/api.ts:819`) +that returns one `AChat` whose `chatInfo` carries the full `GroupInfo` / +`Contact` (with `customData`) and zero items. The Haskell parser accepts +`count=0` (`src/Simplex/Chat/Library/Commands.hs:5210`), and +`getDirectChatLast_` / `getGroupChatLast_` return empty `chatItems` with full +`chatInfo`. + +**B. Genuine multi-record scan** (one site). +`apps/simplex-support-bot/src/cards.ts:131` (`refreshAllCards`) enumerates +groups where `customData.cardItemId && !complete` to refresh in-flight cards +on restart. The Haskell side already supports paginated scans via +`APIGetChats` (`/_get chats {userId} pcc=on|off count=N`, +`src/Simplex/Chat/Library/Commands.hs:4868`). It is currently in +`undocumentedCommands` (`bots/src/API/Docs/Commands.hs:360`), so the codegen +does not emit it for the TypeScript bot library. Confirmed: the chat preview +returned by `getChatPreviews` carries `customData` on `GroupInfo` +(`src/Simplex/Chat/Store/Shared.hs:685`, `toGroupInfo`). + +Active card state is already tracked on each group via `customData.cardItemId` +and `customData.complete` (written through `apiSetGroupCustomData` at +`apps/simplex-support-bot/src/cards.ts:103,231`). No `state.json` schema +change is needed — phase 3 reads exactly the same `customData` it already +writes, just via paginated `APIGetChats` instead of a full `apiListGroups`. + +Per the constraint, `apiListContacts` / `apiListGroups` stay in the nodejs +library unchanged for other consumers. Audit confirmed no callers outside +support-bot use them today. + +## Phase 1 — Plumb `APIGetChats` through the bot library + +The codegen pipeline is test-driven: `tests/APIDocs.hs:41–44` invokes +`testGenerate` against the functions in +`bots/src/API/Docs/Generate/TypeScript.hs`, which writes to: + +- `packages/simplex-chat-client/types/typescript/src/commands.ts` +- `packages/simplex-chat-client/types/typescript/src/responses.ts` +- `packages/simplex-chat-client/types/typescript/src/types.ts` + +Run via `cabal test`. The published `@simplex-chat/types` npm package is +built from this TypeScript source; the copy under +`packages/simplex-chat-nodejs/node_modules/@simplex-chat/types/dist/` is a +downstream build artifact and is **not** edited directly. + +Currently missing from generated TS: +`T.PaginationByTime`, `T.ChatListQuery`, `CC.APIGetChats`, and the +`apiChats` response tag on `ChatResponse`. + +### 1.1 `bots/src/API/Docs/Commands.hs` + +- **Remove** `"APIGetChats",` from `undocumentedCommands` (line 360). +- **Add** an entry under "Chat commands" (next to `APIListContacts` / + `APIListGroups` at lines 145–146). Match the Haskell parser at + `src/Simplex/Chat/Library/Commands.hs:4868`: + + ```haskell + ( "APIGetChats", + [], + "Get chat previews. Supports time-based pagination — use this " <> + "instead of APIListContacts / APIListGroups when scanning at scale.", + ["CRApiChats", "CRChatCmdError"], + [], + Nothing, + "/_get chats " <> Param "userId" + <> OnOffParam "pcc" "pendingConnections" (Just False) + <> Optional "" (" " <> Param "$0") "pagination" + <> Optional "" (" " <> Json "$0") "query" + ) + ``` + + Note: the `query` segment uses `" " <> Json "$0"` (no `"json "` prefix) — + the parser accepts `A.space *> jsonP` directly. + +### 1.2 `bots/src/API/Docs/Types.hs` + +The type universe already references `PaginationByTime` and `ChatListQuery` +in commented form (lines 381, 390 and 592, 602). Uncomment all four lines. +Confirm the constructor-prefix encoding (`STRecord`/`STUnion`, prefix +`""`/`"CLQ"`) matches the existing definitions in +`src/Simplex/Chat/Controller.hs:992,998` and the JSON deriving at line 1661 +(`sumTypeJSON $ dropPrefix "CLQ"`). + +### 1.3 `bots/src/API/Docs/Responses.hs` + +- Uncomment `("CRApiChats", "...")` at line 100. +- Remove `"CRApiChats",` from `undocumentedResponses` at line 123. + +### 1.4 Regenerate TypeScript types + +Run `cabal test` (the `APIDocs` test suite drives generation). Inspect the +diffs in `packages/simplex-chat-client/types/typescript/src/{commands,responses,types}.ts`. +Verify: + +- `T.PaginationByTime` (sum type with `PTLast`/`PTAfter`/`PTBefore`) exists + with a generated `cmdString`. Compare wire format against the Haskell + `paginationByTimeP` at `src/Simplex/Chat/Library/Commands.hs:5216`: + `count=N` | `after=TS count=N` | `before=TS count=N`. +- `T.ChatListQuery` exists with `CLQFilters` / `CLQSearch` JSON-encoded + variants. +- `CC.APIGetChats.cmdString({userId, pendingConnections, pagination, query})` + exists and emits the expected wire format. +- `r.type === "apiChats"` with `r.chats: T.AChat[]` exists in the response + union (drops `CR` prefix per `sumTypeJSON`, + `src/Simplex/Chat/Controller.hs:1743`). + +Bump `@simplex-chat/types` version and re-link / re-build the +`simplex-chat-nodejs` package so the new symbols are available. + +### 1.5 `packages/simplex-chat-nodejs/src/api.ts` + +Add a single method next to `apiListGroups` (line 761): + +```ts +/** + * Get chat previews (paginated). + * Network usage: no. + * + * Prefer this over apiListContacts / apiListGroups for any scan: those + * methods load the entire history into memory and will fail on large DBs. + */ +async apiGetChats( + userId: number, + pagination: T.PaginationByTime, + query: T.ChatListQuery = {type: "filters", favorite: false, unread: false}, + pendingConnections = false, +): Promise { + const r = await this.sendChatCmd( + CC.APIGetChats.cmdString({userId, pendingConnections, pagination, query}) + ) + if (r.type === "apiChats") return r.chats + throw new ChatCommandError("error getting chats", r) +} +``` + +(Exact `T.PaginationByTime` / `T.ChatListQuery` shapes come from the codegen +output of phase 1.4 — verify the discriminator field names before locking +this signature.) + +## Phase 2 — Replace list-then-find with direct lookup + +For every site below, replace `apiList…().find(…)` with +`apiGetChat(ChatType.X, id, 0)`. Treat "not found" — the chat was deleted — +as a clean missing-record case (log + skip). The wire format +`/_get chat #{id} count=0` is already supported. + +### 2.1 Error matcher + +The Haskell `SEContactNotFound` / `SEGroupNotFound` (in +`src/Simplex/Chat/Store/Shared.hs:863` and elsewhere) surface to TS as: + +```ts +err.chatError?.type === "errorStore" + && err.chatError.storeError.type === "groupNotFound" // or "contactNotFound" +``` + +Both discriminators are already present in the generated types +(`types.d.ts:2825` and `:2788`). Add a small helper in +`apps/simplex-support-bot/src/util.ts`: + +```ts +export function isChatNotFound(err: unknown, kind: "group" | "contact"): boolean { + if (!(err instanceof core.ChatAPIError)) return false + if (err.chatError?.type !== "errorStore") return false + const seType = err.chatError.storeError.type + return kind === "group" ? seType === "groupNotFound" : seType === "contactNotFound" +} +``` + +(Strict — does not swallow other `errorStore` variants.) + +### 2.2 Ergonomic wrappers + +Add two thin helpers in `apps/simplex-support-bot/src/util.ts` (the constraint +forbids touching `apiListContacts` / `apiListGroups` in the nodejs library; +keeping these helpers in the support-bot util keeps the library surface +unchanged): + +```ts +export async function getGroupInfo(chat: api.ChatApi, groupId: number): Promise { + try { + const c = await chat.apiGetChat(T.ChatType.Group, groupId, 0) + return c.chatInfo.type === "group" ? c.chatInfo.groupInfo : null + } catch (err) { + if (isChatNotFound(err, "group")) return null + throw err + } +} + +export async function getContact(chat: api.ChatApi, contactId: number): Promise { + try { + const c = await chat.apiGetChat(T.ChatType.Direct, contactId, 0) + return c.chatInfo.type === "direct" ? c.chatInfo.contact : null + } catch (err) { + if (isChatNotFound(err, "contact")) return null + throw err + } +} +``` + +### 2.3 Call-site changes + +All sites must keep their existing `withMainProfile` / `profileMutex` +wrapping where present. + +- **`apps/simplex-support-bot/src/index.ts:165–180`** (Grok contact + resolution). Drop the `apiListContacts(mainUser.userId)` call entirely. If + `state.grokContactId` is set, call `getContact(chat, state.grokContactId)` + inside `profileMutex.runExclusive`. Preserve the existing log lines. +- **`apps/simplex-support-bot/src/index.ts:306–320`** (team member + validation). Loop and `getContact(chat, member.id)` per member. Compare + `displayName` as before. Team rosters are small; N round-trips are fine. +- **`apps/simplex-support-bot/src/index.ts:213–227`** (team group + resolution). Replace `apiListGroups` + `find` with + `getGroupInfo(chat, state.teamGroupId)`. Preserve the "create new group" + fallback when the lookup returns `null`. +- **`apps/simplex-support-bot/src/bot.ts:796–805`** (`handleJoinCommand`). + Replace with `getGroupInfo(chat, targetGroupId)`; same `businessChat` + validation. +- **`apps/simplex-support-bot/src/cards.ts:120`** (`flushOne`). Direct + `getGroupInfo(chat, groupId)` (still inside `withMainProfile`). +- **`apps/simplex-support-bot/src/cards.ts:213`** (`getRawCustomData`). + Direct lookup. **Hot path** — called on every `mergeCustomData` / + `clearCustomData`. Largest single win. +- **`apps/simplex-support-bot/src/cards.ts:251`** (`updateCard`). Direct + lookup. The "Read customData and groupInfo in one apiListGroups call" + comment goes away. + +After phase 2 the bot can boot and operate steadily on a large DB; phase 3 +is purely about startup reconciliation. + +## Phase 3 — Paginate `refreshAllCards` + +`apps/simplex-support-bot/src/cards.ts:131` is the only legitimate +multi-record scan. Convert it to a single bounded `apiGetChats` call: + +```ts +async refreshAllCards(): Promise { + // Scan the most recently active 1000 chats. Active cards live on + // recently-active customer chats by definition — a card stays open while + // the conversation is in flight. If the bot has been offline long enough + // that an active card has fallen outside the recent-1000 window, that + // card refreshes lazily on the next customer message (which moves the + // chat back into the recent window). + const chats = await this.withMainProfile(() => + this.chat.apiGetChats( + this.mainUserId, + {type: "last", count: 1000}, + ) + ) + const activeCards: {groupId: number; cardItemId: number}[] = [] + for (const c of chats) { + if (c.chatInfo.type !== "group") continue + const customData = c.chatInfo.groupInfo.customData as Record | undefined + if (customData + && typeof customData.cardItemId === "number" + && !customData.complete) { + activeCards.push({ + groupId: c.chatInfo.groupInfo.groupId, + cardItemId: customData.cardItemId, + }) + } + } + // (sort and refresh loop unchanged) +} +``` + +`count = 1000` per the constraint. No `state.json` schema change. Card +status remains entirely on the group's `customData` (`cardItemId`, +`complete`), which is what the bot already reads and writes. + +## Phase 4 — Verification + +### 4.1 Stress test + +Existing tests use `MockChatApi` (`apps/simplex-support-bot/bot.test.ts:24`), +which is in-memory and won't exercise the native binding. A meaningful +stress test needs a real `ChatApi.init` against Postgres. + +Add a new test file (e.g. +`apps/simplex-support-bot/test/stress.test.ts`) that: + +1. Starts an ephemeral Postgres or uses an existing test DB. +2. Calls `ChatApi.init` and seeds N synthetic groups + contacts via the + chat API. (No existing seeding helper — write one.) Reasonable N: 20k + each, large enough to expose the marshaling cliff but not so large that + the test takes minutes. +3. Boots the support-bot main flow against this DB and asserts: startup + completes within a wall-clock budget; resident memory stays bounded; + no native error. + +This is new infrastructure — keep scope tight. If standing up Postgres in +CI is too heavy, run as a manual stress harness rather than a CI test. + +### 4.2 Production replay + +Replay against (a copy of) the affected production DB. Confirm the bot +starts and the team group / Grok contact / team members all resolve. + +### 4.3 Smoke tests + +Existing functional flows via `bot.test.ts` continue to pass after the +phase-2 changes. Manually exercise: + +- Business-request acceptance. +- `/join` validation (the changed `bot.ts:799` path). +- Card create/update/complete cycle (`cards.ts` hot path). +- Restart-time card refresh (`refreshAllCards`). + +## Risks and footguns + +- **`/_get chats` parser default is `PTLast 5000`** + (`src/Simplex/Chat/Library/Commands.hs:4872`). Even 5000 previews can be + heavy. Support-bot now always passes an explicit `count=1000`, but the + default itself remains a footgun for other callers — flag for follow-up; + not changed here. +- **`apiListMembers` is per-group, not per-DB.** Used at `bot.ts:629,825` + and `cards.ts:165`. Bounded by group membership, not history size, so + out of scope for this fix. Flag if customer groups grow huge (>1000 + members) — would warrant a paginated members API at that point. +- **Codegen output sanity.** Phase 1.4 must be inspected by hand — the + generated `cmdString` for `T.PaginationByTime` and the `r.type` / + `r.chats` shape on the response side are the integration points the rest + of the plan depends on. Do not skip eyeballing the diff. +- **`apiGetChat(..., 0)` semantics on a non-existent chatId.** Verified: + the error tag is `chatError.type === "errorStore"` with + `storeError.type === "groupNotFound"` or `"contactNotFound"`. Both + discriminators already exist in the generated types + (`types.d.ts:2825,2788`). `isChatNotFound` matches them precisely; do + not loosen it. +- **Native binding crash hypothesis is unverified.** The literal "Unknown + failure" string is not in this tree. Most likely V8/N-API surfacing a + string-allocation or JSON-parse failure. The fix in this plan addresses + the proximate cause (oversized response payload) regardless of the exact + surfacing path; if the same error reappears after the fix, dig into the + binding's `OnOK` handler to add explicit size-check / better diagnostics. +- **`@simplex-chat/types` package version bump.** Phase 1.4 produces + TypeScript changes in `packages/simplex-chat-client/types/typescript/`. + Bumping the version and re-publishing (or rebuilding locally) is required + before phase 1.5 lands. Coordinate the release sequence. + +## Out of scope + +- Deprecating or paginating `apiListContacts` / `apiListGroups` in the + nodejs library. They stay as-is; only support-bot stops calling them. +- Lowering the `/_get chats` parser default from `PTLast 5000`. +- Adding a paginated members API. +- Native binding diagnostics for oversized responses. diff --git a/plans/2026-05-07-desktop-rtl-composer-fix.md b/plans/2026-05-07-desktop-rtl-composer-fix.md new file mode 100644 index 0000000000..b593be481d --- /dev/null +++ b/plans/2026-05-07-desktop-rtl-composer-fix.md @@ -0,0 +1,390 @@ +# Fix #4137 — desktop: RTL text rendering under send button + +Target file: +`apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt` + +--- + +## 1. Problem statement + +### 1.1 Symptom + +On desktop, when the user types right-to-left text (Arabic, Hebrew, +Persian) in the chat composer **while the global system locale is LTR**, +the first characters of the typed text are rendered **under the send +button** at the bottom-right corner and become invisible while typing. + +The same defect places the voice-preview / disabled-state +`ComposeOverlay` text on the wrong horizontal side in this configuration. + +### 1.2 Configurations affected + +Tested 4 combinations of (global locale × typed-text direction): + +| Global locale | Typed text | Behavior | +|---------------|------------|----------| +| LTR | LTR | OK | +| LTR | RTL | **broken** — text under send button | +| RTL | LTR | OK | +| RTL | RTL | OK | + +Only the LTR-locale + RTL-text combination is broken. This is the +configuration where the **inner text rendering direction** (forced RTL by +`decorationBox`) **disagrees** with the **outer layout direction** (LTR). + +### 1.3 Why it matters + +- Persian/Arabic/Hebrew users on a non-localized OS (very common: most + desktop installs default to English) cannot see the start of their own + message until it grows past the send button. +- The composer is the most-used input in the app; this is a daily + papercut for the affected user population. + +--- + +## 2. Root cause + +A direction-resolution decoupling introduced by an unrelated refactor. +Two commits matter: + +### 2.1 The original RTL fix — #4675 (`2ae5a8bff`, Aug 2024) + +Added padding logic *inside* a forced-RTL scope: + +```kotlin +CompositionLocalProvider(LocalLayoutDirection provides Rtl) { + Column(Modifier.weight(1f).padding(start = startPadding, end = endPadding)) { + TextFieldDecorationBox(...) + } +} +``` + +Inside that scope `start` resolves to the **right** edge. So setting +`startPadding = 50.dp` for the RTL-text + LTR-locale case correctly +reserved 50dp on the visual right — same side as the send button. + +**The padding side and button side were aligned by accident.** `start` +tracked the forced-RTL direction in the same way that `Alignment.BottomEnd` +in `SendMsgView.kt:120` tracked the global direction — and the two +happened to coincide *as long as those directions were the same.* The +pre-existing rule expressed in code was effectively "padding follows +typed-text direction," which was equivalent to "padding follows button +side" only when the inner forced direction and the outer global direction +agreed. + +### 2.2 The breaking refactor — #5051 edge-to-edge (`4162bccc4`, Nov 2024) + +The padding modifier was lifted **out** of the forced-RTL scope onto the +outer `BasicTextField` (the wrapping `Column` and `Row` were removed). +The outer modifier now resolves `start`/`end` against the **global** +layout direction, but `decorationBox` still forces +`LayoutDirection.Rtl` for RTL characters internally. + +In LTR-global + RTL-text: + +- `padding(start = 50.dp)` → 50dp reserved on visual **left** +- Text right-aligned by forced-RTL `decorationBox` → renders against + visual **right** +- 0dp on the right → text under the send button (which is at + `Alignment.BottomEnd` in LTR global = visual right) + +The compensation logic written for the inner-scope semantics silently +became wrong when the modifier moved outward. Code compiled, tests passed, +behavior diverged. + +### 2.3 The actual invariant the layout obeys + +Reading the layout call graph (`SendMsgView` → `PlatformTextField`): + +- `SendMsgView.kt:120` — `Box(Modifier.align(Alignment.BottomEnd)...)` + places the send button using the **global** layout direction. +- `PlatformTextField.desktop.kt` — `BasicTextField` modifier chain is + applied in the **global** layout direction. + +The constraint is therefore exactly one rule: + +> **The textfield must reserve space on the global layout direction's +> `end` — the same side `Alignment.BottomEnd` resolves to in the parent +> `Box`.** + +Pre-PR code expressed a different (wrong) rule — "padding follows +typed-text direction" — which agreed with the actual invariant only when +no RTL-text/LTR-locale mismatch existed. The 4 of 4 case failure → 1 of 4 +case failure shape is the signature of this kind of accidental alignment. + +### 2.4 Why this is structural, not a typo + +The defect is not a missing case — it is the **wrong rule**. Adding a +new branch (e.g. "if RTL-text + LTR-locale, swap padding sides +*again*") would silence the symptom while leaving the wrong rule in +place. The fix is to delete the wrong rule and write the actual +invariant. + +--- + +## 3. Solution summary + +Make the two conditional assignments that compute `startPadding` and +`endPadding` unconditional, taking the values they already produced in +the `else` branch: + +```kotlin +val startPadding = 0.dp +val endPadding = startEndPadding +``` + +The surrounding code is unchanged — `startEndPadding`'s computation, +the `PaddingValues(startPadding, 12.dp, endPadding, 0.dp)` construction, +the `.padding(start = startPadding, end = endPadding)` modifier call, +and the original two-line comment all stay verbatim. + +Master's `if (isRtlByCharacters && isLtrGlobally)` predicate split each +of `startPadding` and `endPadding` into two branches. In cases 1, 3, 4 +the predicate is `false` and master takes the `else` branch — exactly +the values the surgical version produces unconditionally. Only case 2 +(the bug) takes the `then` branch, and that branch reserves space on +the wrong horizontal side. Removing the predicate removes only case 2's +wrong values; cases 1/3/4 are byte-identical to master. + +The 95dp/50dp distinction is preserved verbatim through `startEndPadding`, +which is unchanged. + +`ComposeOverlay` (called twice at the bottom of `PlatformTextField`) +reuses the same `padding` value — its placement is corrected for the +same reason without an extra change. + +**Net effect**: 2 lines changed. + +--- + +## 4. Detailed technical design + +### 4.1 Behavior matrix (post-fix) + +| Case | Locale | Text | Master `(start, end)` | Surgical `(start, end)` | Button side | +|------|--------|------|-----------------------|-------------------------|-------------| +| 1 | LTR | LTR | `(0, 50)` | `(0, 50)` | right ✓ same | +| 2 | LTR | RTL | `(50, 0)` | `(0, 50)` | right ✓ **fix** | +| 2′ | LTR | RTL + empty + voice | `(95, 0)` | `(0, 95)` | right ✓ **fix** | +| 3 | RTL | LTR | `(0, 50)` | `(0, 50)` | left ✓ same | +| 4 | RTL | RTL | `(0, 50)` | `(0, 50)` | left ✓ same | + +Three of the four pre-PR cases are byte-identical to the new code. +Only the broken case (LTR locale + RTL text) flips from `(50, 0)` to +`(0, 50)`, which matches the side where the send button resolves. + +### 4.2 Why the 95dp condition stays exactly as-is + +The 95dp special case fires only in RTL-text + LTR-locale + empty + +voice-button. In every other configuration, the placeholder text +either left-aligns (no collision with the right-side voice button row) +or sits on the visual side opposite to the buttons (RTL global puts +buttons on the left while forced-RTL placeholder displays on visual +right). + +Only the RTL-text + LTR-global case puts a right-aligned placeholder +on the same side as the wider voice-button row. The condition is +intrinsic to the architecture (forced-RTL inside `decorationBox` while +the outer layout is global LTR), not a bug — it must be preserved. + +### 4.3 What is *not* changed + +Out of scope for #4137 — listed for clarity: + +- The `CompositionLocalProvider` inside `decorationBox` that forces + `LayoutDirection.Rtl` for RTL-by-characters input (the BiDi-detection + workaround from #4675 itself). +- `lastTimeWasRtlByCharacters` state and `isRtl` detection on the first + 50 characters of the message. +- The `ComposeOverlay` composable — it inherits the corrected + `padding`. +- `SendMsgView`, the `Alignment.BottomEnd` send button placement, and + the voice-button row layout. +- The Android implementation (`PlatformTextField.android.kt`) — uses + a native Android `EditText` with `setPaddingRelative`, which + resolves against the view's own layout direction; behavior is + unaffected and out of scope. + +### 4.4 Properties of the resulting code + +- The two adjacent conditional assignments dispatching on + `isRtlByCharacters && isLtrGlobally` (one for `startPadding`, one for + `endPadding`) become unconditional. The predicate is removed; the + `else` branch's values are lifted to the bare assignments. +- All four locals (`startEndPadding`, `startPadding`, `endPadding`, + `padding`) keep the same names and continue to exist. +- The `PaddingValues(startPadding, 12.dp, endPadding, 0.dp)` call and + the `.padding(start = startPadding, end = endPadding)` modifier are + unchanged. +- The original two-line comment is unchanged. "padding from right side + should be bigger" remains accurate — `endPadding` is still `95.dp` + vs `50.dp` under the same condition as before, just consistently on + the global end side. +- No behavior is removed: RTL detection, the `decorationBox` direction + override, overlay rendering, and the empty-text/voice-button 95dp + expansion are all retained verbatim. +- Diff size: 2 lines changed, one file. No reformatting of unrelated + code. + +### 4.5 Risk surface + +- **Compose 1.7.x BiDi engine** — unchanged; we still rely on + `decorationBox`'s forced direction for right-alignment of typed RTL + text. No new BiDi dependency. +- **Padding API** — `Modifier.padding(end = X.dp)` and + `PaddingValues(start, top, end, bottom)` are stable Compose APIs. +- **Direction resolution** — `Modifier.padding`'s start/end have + resolved against the enclosing `LocalLayoutDirection` since Compose + Foundation 1.0; no version-sensitive behavior. +- **Cross-platform** — Android implementation uses a native + `EditText`; no shared change required. + +--- + +## 5. Detailed implementation plan + +### 5.1 The exact edit + +File: +`apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt` + +**Lines 89–90 — replace 2 lines:** + +```kotlin +// remove + val startPadding = if (isRtlByCharacters && isLtrGlobally) startEndPadding else 0.dp + val endPadding = if (isRtlByCharacters && isLtrGlobally) 0.dp else startEndPadding + +// add + val startPadding = 0.dp + val endPadding = startEndPadding +``` + +No other lines change. No imports added or removed. The comment, the +`startEndPadding` computation, the `PaddingValues` construction, and +the `.padding(start = startPadding, end = endPadding)` modifier are +all preserved verbatim. + +### 5.2 Steps + +1. Edit `PlatformTextField.desktop.kt` at the site above (lines 89–90). +2. Build desktop module: + `cd apps/multiplatform && ./gradlew :common:desktopMainClasses` +3. Run desktop app on an LTR system locale; type + `متن راست به چپ` in the composer; verify all characters visible. +4. Type ASCII; verify no regression. +5. Switch system locale to Arabic/Persian/Hebrew; repeat both inputs; + verify send button and reservation flip together to the visual + left, with no overlap. +6. Trigger voice preview / disabled-state placeholder in each + configuration; verify the overlay text is on the side opposite + the send button. +7. Commit on a topic branch (`nd/fix-RTL`); PR title: + `desktop: fix RTL text rendering under the send button`; reference + `Fixes #4137`. + +### 5.3 Test matrix to verify manually + +| # | Locale | Typed text | Empty + voice? | Expectation | +|---|--------|-----------|----------------|-------------| +| 1 | LTR | ASCII | n/a | unchanged from current | +| 2 | LTR | RTL chars | no | chars visible, no overlap with right-side button | +| 3 | LTR | empty | yes | placeholder + voice-button row both visible | +| 4 | LTR | (was RTL) → empty | yes | placeholder clears 95dp on right (sticky `lastTimeWasRtlByCharacters`) | +| 5 | RTL | ASCII | no | unchanged | +| 6 | RTL | RTL chars | no | unchanged | +| 7 | RTL | empty | yes | unchanged | + +### 5.4 Rollback + +Revert is one commit and one file. Behavior reverts cleanly. + +--- + +## 6. Alternative approaches considered + +### 6.1 Chosen approach — drop the buggy `then` branch (§3) + +The 2-line surgical change. Removes the predicate from the +`startPadding` and `endPadding` assignments, keeping the (correct) +`else` branch values as the unconditional definition. Smallest +possible diff; preserves all variable names, the comment, the +`PaddingValues` call, and the `.padding(start, end)` modifier. +Fixes the overlay placement as a free byproduct. + +### 6.2 Re-couple padding to inner forced direction by wrapping `BasicTextField` + +Move the `CompositionLocalProvider(LocalLayoutDirection = Rtl)` *outside* +`BasicTextField` rather than inside `decorationBox`. The outer +`.padding(start, end)` would then resolve in the same direction as the +inner text, restoring the pre-#5051 invariant and letting the +historical `start = 50.dp / end = 0.dp` swap work again. + +**Pros**: padding-vs-text consistency at the source. + +**Cons**: also flips `fillMaxWidth`, `focusRequester`, `onPreviewKeyEvent`, +and the parent `Box`'s `Alignment.BottomEnd` resolution direction is +**still global** — so the textfield and the button align against +different directions, moving the mismatch instead of removing it. +Bigger refactor, broader test surface, no net gain. **Rejected.** + +### 6.3 Remove the forced-RTL override; rely on Compose BiDi + +Delete the `CompositionLocalProvider` inside `decorationBox`. Let +Compose's BiDi engine right-align RTL paragraphs without forcing a +paragraph direction. Then `start`/`end` resolve consistently against +the global direction everywhere; `isRtlByCharacters`, +`lastTimeWasRtlByCharacters`, and the 95dp special case can all be +deleted. + +**Pros**: largest simplification — eliminates the entire BiDi-detection +state machine and the 95dp branch. + +**Cons**: depends on Compose Desktop 1.7.x BiDi engine matching what +#4675 originally needed to enforce. If automatic BiDi is insufficient +(e.g. mixed Latin-RTL paragraphs, neutral characters at paragraph start, +numbers in RTL paragraphs), regressions reappear. Requires manual +verification across all the cases #4675 originally fixed. Out of scope +for #4137. **Reasonable follow-up; not part of this fix.** + +### 6.4 Derive padding from measured button-row width + +Refactor `SendMsgView` so the textfield's reservation comes from the +**measured** width of the button row (via `SubcomposeLayout` or shared +state), instead of hard-coded 50/95dp. The textfield would reserve +exactly as much as the buttons need, regardless of direction or button +configuration. + +**Pros**: removes the 50/95dp magic numbers and the +`showVoiceButton`-dependent branch. Self-correcting if the button row +ever changes. + +**Cons**: significantly larger refactor; `SubcomposeLayout` adds cost +to a frequently-recomposing view; doesn't fix the bug at hand any +better than §6.1. **Reasonable longer-term cleanup; not part of this +fix.** + +### 6.5 Add a third special case for the failing combination + +`if isRtlByCharacters && isLtrGlobally then padding(end=50) else +padding(start=startPadding, end=endPadding)`. + +**Pros**: one-line behavior fix. + +**Cons**: leaves the wrong rule in place plus a workaround on top. +Three branches where one suffices, and the underlying defect — padding +following typed-text direction instead of button side — is preserved +and now harder to spot. **Rejected as a workaround.** + +--- + +## 7. Recommendation + +Implement §3 (the chosen approach). It is the minimal structural +root-cause fix, also corrects the overlay placement as a free byproduct, +and removes the wrong-side `then` branch from both `startPadding` and +`endPadding`. + +Defer §6.3 and §6.4 to separate PRs if desired — both are reasonable +cleanups but are not necessary to fix #4137 and would expand the blast +radius beyond the bug. diff --git a/plans/2026-05-07-fullscreen-viewer-wrong-image.md b/plans/2026-05-07-fullscreen-viewer-wrong-image.md new file mode 100644 index 0000000000..068d2b07c7 --- /dev/null +++ b/plans/2026-05-07-fullscreen-viewer-wrong-image.md @@ -0,0 +1,135 @@ +# Fullscreen image viewer: opens the wrong image + +Design doc for the fix shipped in PR #6869. + +## Problem + +The fullscreen image viewer occasionally opened the chat's oldest media +instead of the image the user tapped. Reproductions were intermittent — +the gating condition turned out to be the runtime state of the +*immediately-older* sibling of the tapped item. + +## Background — pager state model + +`providerForGallery` (`apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt:3537`) +backs the fullscreen viewer with a virtual pager of 10000 pages. The +pager's state is two variables, captured in a closure: + +- `initialIndex` — pager page that maps to the anchor item; starts at 5000. +- `initialChatId` — id of the anchored chat item; starts at the tapped item. + +Invariant: page `initialIndex` always shows item `initialChatId`. Other +pages are computed by walking `chatItems` older / newer from the anchor +via the local `item()` helper. + +`scrollToStart()` is called by `ImageFullScreenView.kt` to lock the +pager's leftward boundary at the user's current item, in two situations: + +- **Init probe** (`ImageFullScreenView.kt:48-55`) — at viewer open, if + `getMedia(initialIndex - 1) == null` (no older sibling reachable), + reposition so the tapped item becomes page 0. +- **Runtime branch** (`ImageFullScreenView.kt:97-112`) — during scroll, + if `getMedia(index) == null` while the user is at `index + 1`, lock + the pager so the null page isn't reachable. + +Both callers want the same outcome: **page 0 = the user's current +anchor item**, leftward = unreachable. + +## Root cause + +Pre-fix body of `scrollToStart`: + +```kotlin +override fun scrollToStart() { + initialIndex = 0 + initialChatId = chatItems.firstOrNull { canShowMedia(it) }?.id ?: return +} +``` + +The second line rewrote `initialChatId` to the chat's *oldest showable +media* — not the user's current anchor. This mismatched what both +callers wanted. It happened to coincide with the correct behavior when +the anchor already was the chat's oldest showable, which is why the bug +masked itself for years. + +The bug surfaced when the init probe fired for a non-boundary reason: + +- The immediately-older sibling existed and passed `canShowMedia` (file + marked loaded; file path resolved or remote was connected). +- But `getLoadedImage` returned `null` at decode time (undecodable + bytes, missing file on disk, crypto error). +- `getMedia(initialIndex - 1)` therefore returned `null`. +- The probe misread that null as "no older sibling exists" and called + `scrollToStart()`. +- `scrollToStart` rewrote `initialChatId` to the chat's oldest showable. +- Page 0 of the pager rendered that oldest item — the wrong image. + +## Fix + +Delete the second line. `scrollToStart` becomes: + +```kotlin +override fun scrollToStart() { + initialIndex = 0 +} +``` + +`initialChatId` is preserved across the call. Page 0 now maps to the +current anchor — exactly what both callers wanted from the start. + +## Why this is correct for both callers + +- **Init probe.** Before the call, `initialChatId` is the tapped item. + After the call, page 0 = tapped item. ✓ +- **Runtime branch.** Before the call, `currentPageChanged` has already + updated `initialChatId` to the user's currently visible item. After + the call, page 0 = current item; the user's view is preserved with no + visible jump. (Pre-fix the user got teleported to the chat's oldest + media when a null sibling tripped this branch — a latent UX bug + resolved by the same one-line change.) + +## Why a wider structural change is not in scope here + +`getMedia` returns `null` for two distinct conditions: (a) navigation +found no showable item, (b) navigation found one but decode failed. A +deeper refactor would let consumers distinguish these. That refactor is +deliberately out of scope for this fix: + +- The user-visible bug (wrong image) is fully resolved by the one-line + change. No additional code is required to address the report. +- The remaining symptom — locking the user out of older loadable items + behind one that fails to decode — is mild, pre-existing, and not part + of the report. If it becomes user-visible, address it in a follow-up. +- A wider refactor would expand the diff, the review surface, and the + regression risk for a fix that needs to ship promptly. +- `good-code-v5.md`: *"Find the minimal change. The smallest structural + modification that achieves the goal."* The smallest modification that + resolves the reported bug is the deletion of one line. + +## Verification + +`apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ProviderForGalleryTest.kt`: + +- `testScrollToStartPreservesAnchor` — drives the public provider + interface: moves the anchor off `cItemId` via `currentPageChanged`, + calls `scrollToStart`, then reads the anchor back through `onDismiss`'s + `scrollTo` callback. Pre-fix would observe `scrollTo(2)` (the chat's + oldest); post-fix `scrollTo(1)` (anchor preserved). +- `testOnDismissOnActiveItemDoesNotScroll` — pins the `onDismiss` + early-return contract that the regression test reads through. + +Manual sanity (Android + desktop): tap newest / oldest / a middle image +in a chat with multiple media — fullscreen opens on the tapped image in +each case; swipe in both directions still works. + +## Alternatives considered and rejected + +- **Distinguish "no item" from "load failed" inside `getMedia`.** + Requires either a return-type redesign (sealed result type) or an + added query method on the interface. Both expand the diff well beyond + what the user-visible bug requires. Deferred to a possible follow-up + if the milder remaining symptom is reported. +- **Hoist the local `item()` helper to a top-level testable function.** + The regression test exercises the public provider interface and + reads the anchor back via `onDismiss`'s `scrollTo` callback, so no + internal extraction is needed for testability. diff --git a/plans/2026-05-08-desktop-text-selection-id-anchored.md b/plans/2026-05-08-desktop-text-selection-id-anchored.md new file mode 100644 index 0000000000..0d986f62ed --- /dev/null +++ b/plans/2026-05-08-desktop-text-selection-id-anchored.md @@ -0,0 +1,145 @@ +# Desktop Text Selection — Anchor by Item Id + +## 1. The bug + +`SelectionRange` stored two **positional** indices into the reversed merged-items list: + +```kotlin +data class SelectionRange( + val startIndex: Int, + val startOffset: Int, + val endIndex: Int, + val endOffset: Int +) +``` + +`reversedChatItems` grows from the front: a new message is prepended at index 0, every existing item shifts +1. Selection indices were never adjusted, so once the user had a selection on a message and another message arrived (or was sent), the indices kept pointing to the same numerical positions while the items at those positions had changed. The highlight (and the copy result) silently moved onto neighbouring messages. + +Same root cause for the deletion case: removing an item from the list left selection indices pointing into a different item. + +## 2. Root cause + +Selection is **about items**, not positions. Storing positions into a list whose front grows is structurally wrong. The data structure must encode the stable identity (`ChatItem.id`), not the volatile position. + +Two ingredients are mandatory for any correct fix: + +1. **Remember which items** are anchor and focus (their stable `ChatItem.id`s). +2. **Update the positional indices** when the list mutates, so that everything downstream that reads `range.startIndex` / `range.endIndex` (highlight rendering, copy iteration, snap, copy-button placement, anchor/focus detection in `setupItemSelection` / `setupEmojiSelection`, drag direction in `SelectionCopyButton`) stays correct. + +Anything beyond this is structural overreach. + +## 3. Approaches considered + +| # | Approach | Note | +|---|----------|------| +| A | Replace positional indices with ids in `SelectionRange`; cache items on the manager via `mutableStateOf`; expose indices via `derivedStateOf`; rename every reader from `range?.startIndex` to `manager.startIndex`; move top-level `selectedRange` into the manager as a method. | Structurally clean (single source of truth = ids), but renames every reader and moves a function for no behaviour reason. Ripples through `setupItemSelection`, `setupEmojiSelection`, `SelectionCopyButton`, `getSelectedCopiedText`, `snapSelection`, `copyButtonOffset`. | +| B | Same as A but replace the cached `var items` with `var mergedItemsState: State?` (mirrors the existing `listState` field; eliminates duplicated state and the items-sync line in `SideEffect`). | Marginal improvement; the cost is still the renames and the function move, neither of which the bug requires. | +| C | **Final** — keep positional indices in `SelectionRange`, **add** `startItemId, endItemId` alongside them; resync the indices to the items they were anchored to on every recomposition via a `SideEffect`. | Every existing reader of `range.startIndex` / `range.endIndex` keeps working unchanged. The fix is a pure addition. | + +Approach C accepts one piece of structural duplication that A and B do not have: anchor ids and positional indices coexist in `SelectionRange`, kept consistent by `resyncIndices`. For a bug-fix change, the trade-off favours diff minimality — migrating to a single source of truth (ids only, indices derived) is a separate refactor that should not be bundled with a fix. + +## 4. Final implementation + +### 4.1 `SelectionRange` — two new fields + +```kotlin +data class SelectionRange( + val startIndex: Int, + val startItemId: Long, // NEW — stable anchor for the selection start + val startOffset: Int, + val endIndex: Int, + val endItemId: Long, // NEW — stable anchor for the selection focus + val endOffset: Int, +) +``` + +Existing `r.copy(startOffset = …)`, `r.copy(endOffset = …)`, `r.copy(startOffset = …, endOffset = …)` calls in `setAnchorOffset` / `updateFocusOffset` / `snapSelection` automatically preserve the new fields (data-class `copy` semantics). No change to those methods. + +### 4.2 `SelectionManager` — one new field, two body additions, one new method + +```kotlin +var mergedItemsState: State? = null // mirrors existing listState +``` + +`startSelection` looks up the id once at click time: + +```kotlin +fun startSelection(startIndex: Int, anchorY: Float, anchorX: Float) { + val id = mergedItemsState?.value?.items?.getOrNull(startIndex)?.newest()?.item?.id ?: return + range = SelectionRange(startIndex, id, -1, startIndex, id, -1) + selectionState = SelectionState.Selecting + anchorWindowY = anchorY + anchorWindowX = anchorX +} +``` + +`updateFocusIndex` updates `endItemId` whenever it updates `endIndex` (called both from `updateDragFocus` and from the scroll snapshotFlow — both paths covered by this single method): + +```kotlin +fun updateFocusIndex(index: Int) { + val r = range ?: return + val id = mergedItemsState?.value?.items?.getOrNull(index)?.newest()?.item?.id ?: return + range = r.copy(endIndex = index, endItemId = id) +} +``` + +New method: + +```kotlin +fun resyncIndices() { + val r = range ?: return + val items = mergedItemsState?.value?.items ?: return + val newStartIndex = items.indexOfFirst { it.newest().item.id == r.startItemId } + val newEndIndex = items.indexOfFirst { it.newest().item.id == r.endItemId } + if (newStartIndex < 0 || newEndIndex < 0) clearSelection() + else range = r.copy(startIndex = newStartIndex, endIndex = newEndIndex) +} +``` + +### 4.3 `SelectionHandler` — three new lines + +```kotlin +manager.listState = listState +manager.mergedItemsState = mergedItems // NEW — wires items into the manager +manager.onCopySelection = { … } + +// Resync after the items list mutates (new message arrives, item deleted). +SideEffect { manager.resyncIndices() } // NEW — the trigger +``` + +### 4.4 What is *not* changed + +- `selectedRange(range, index)` — still a top-level function with its existing signature. +- `getSelectedCopiedText(items, revealedItems, linkMode)` — same signature, same body. +- `snapSelection(items, linkMode)` — same signature, same body. +- `copyButtonOffset(...)` — uses `r.endIndex` directly; no change. +- `setupItemSelection`, `setupEmojiSelection`, `SelectionCopyButton` — every `range?.startIndex` / `range?.endIndex` reference is preserved verbatim. +- `startDragSelection`, `updateDragFocus`, `startSelection` (signature), `updateFocusIndex` (signature) — unchanged. `mergedItemsState` is reached via the manager's own field, so callers don't thread items. + +This is the structural property that compresses the diff: callers see no API change, and the file's structure (top-level `selectedRange`, top-level `selectedItemCopiedText`, top-level `snapOffset`, top-level extension helpers) is untouched. + +## 5. Why this works in Compose + +`SideEffect { manager.resyncIndices() }` runs after every successful composition of `SelectionHandler`. `SelectionHandler` returns a `Modifier` (non-Unit return → non-skippable), so it re-runs whenever its caller (`ChatView`) re-runs, which `ChatView` does whenever `mergedItems.value` changes (it iterates the items list directly). Within the same Compose frame, the `SideEffect` mutation of `range` invalidates the children that read `range`, and Compose re-runs them to convergence before commit. Net visible result: the selection highlight stays on the originally selected items on the same frame the new message arrives — same fidelity as a `derivedStateOf`-based approach, no observable lag. + +`mergedItemsState` is a plain `var` (not `mutableStateOf`) — this is fine because (a) it is reassigned on every recomposition of `SelectionHandler` to the same `State` reference, and (b) the values inside it are read through `State.value`, which Compose tracks. The pattern is identical to the existing `var listState: State? = null` field on the manager. + +## 6. Behaviour changes — full inventory + +1. **Selection follows the original messages when the items list mutates.** This is the bug fix. +2. **Selection clears if either anchor item is removed from the list** (e.g. message deleted from another session). Previously, indices silently slid onto neighbouring messages. The new behaviour is `clearSelection()` when `indexOfFirst` returns -1. This is a side-effect of anchoring by id — once the anchor is gone, "the selection" is no longer well-defined. It is the same class of bug as #6.1 and is fixed by the same mechanism. +3. **Defensive `?: return` in `startSelection` and `updateFocusIndex`** when the id lookup fails. In practice this branch is unreachable: `mergedItemsState` is wired before any user input; the index passed in always comes from `resolveIndexAtY` (which only returns visible-item indices); `newest().item` is non-null for any merged item. No observable change, but worth flagging for completeness. + +Nothing else changes. Verified by reading the diff against master line-by-line. + +## 7. Verification + +1. **Linux desktop build** succeeded end-to-end, producing `SimpleX_Chat-x86_64.AppImage`. No compilation errors, no Compose runtime issues from the new field on the manager or the new fields on `SelectionRange`. +2. **Manual flow against the test plan**: selection persists across `new-message-arrives`, `new-message-sent`, multi-item span; deletion clears (see §6.2); drag-select & copy button behaviour preserved. + +## 8. Trade-offs and follow-ups + +The two pieces of structural debt this change knowingly leaves in place: + +1. **Anchor ids and positional indices coexist in `SelectionRange`.** Single source of truth would store only ids and derive indices on read. The cost of unifying is the rename and function-move churn, which is independent of this bug. A follow-up could collapse these into ids-only without behaviour change, scoped to its own commit. +2. **`resyncIndices` runs on every recomposition of `SelectionHandler`.** The two `indexOfFirst` calls are O(n) on the items list. If profiling ever shows this on a hot path, the cheap fix is to gate on the pointer identity of the items list (`if (lastResyncedItems !== items) { … }`) — one extra field, one branch. Not worth doing speculatively. diff --git a/plans/2026-05-08-fix-select-in-reports.md b/plans/2026-05-08-fix-select-in-reports.md new file mode 100644 index 0000000000..a1731c34d8 --- /dev/null +++ b/plans/2026-05-08-fix-select-in-reports.md @@ -0,0 +1,182 @@ +# Fix copying selected text in reports + +PR: [#6863](https://github.com/simplex-chat/simplex-chat/pull/6863) · branch `nd/fix-select-in-reports` · final commit `96d6f3222` + +## 1. Problem statement + +Report items in desktop render as a red italic *reason prefix* followed by the user's comment, e.g. `Spam: hi @alice`. The user reported that selecting `Spam: test` and pressing Ctrl-C / clicking the copy button placed only `test` on the clipboard — the `Spam: ` prefix was silently dropped. Selecting *only* the prefix produced an empty clipboard. + +A second symptom existed for any report whose comment contained a transformed segment (mention with `localAlias`, link with `showText`): dragging a selection boundary inside that segment snapped to the wrong character on release, then copy emitted the wrong text. + +Both symptoms have a single cause and the bug is desktop-only because the touch UI does not use this selection path. + +## 2. Solution summary + +`MarkdownText` builds the on-screen `AnnotatedString` as `[prefix][body]` (one composable, one layout). Compose's `layout.getOffsetForPosition(...)` therefore returns selection offsets in **display-text space**, which includes the prefix. Pre-PR, `selectedItemCopiedText` and `snapOffset` walked `ci.formattedText` from `displayOffset = 0` — i.e. they treated those offsets as **prefix-excluded body offsets**. Every offset for a report was off by `prefix.length`. + +The fix is one structural realisation: the prefix is the **leading display-space segment**, so the loop that walks `ci.formattedText` must start at `displayOffset = prefix.length`, and any portion of the selection that falls in `[0, prefix.length)` must be emitted by appending a slice of the prefix string before the loop runs. + +To prevent the same silent decoupling from re-emerging, the prefix string itself is extracted into a single source of truth — `itemPrefixText(ci)` — used by every call site that either renders or measures the prefix. + +## 3. Detailed tech design + +### 3.1 Where the offsets come from + +``` +Compose layout + └─> SelectableText / ClickableText (TextItemView.kt) + └─> getOffsetForPosition(localPos) // returns display-space offset + └─> SelectionManager.setAnchorOffset / updateFocusOffset + └─> selectedRange(...) → IntRange in display space + └─> selectedItemCopiedText(ci, sel, linkMode) // FIX site #1 + └─> snapOffset(ci, off, linkMode, expandRight) // FIX site #2 +``` + +`MarkdownText` (TextItemView.kt) builds the `AnnotatedString` in this order: + +``` +inlineContent — never present for report items +appendSender(...) — null for the CIMarkdownText path +prefix (AnnotatedString) — "${reason}: " for reports, null otherwise +text / formatted segments — the body +typingIndicator (live only) — past selectableEnd +reserve (timestamp space) — past selectableEnd +``` + +For non-report items the prefix is null and the existing identity `displayOffset = 0` holds. For reports, the body's first character lives at display offset `prefix.length`. + +### 3.2 The minimal structural change + +Pre-PR loop: + +```kotlin +var displayOffset = 0 +for (ft in formattedText) { + val segDisplay = itemSegmentDisplayText(ft, ci, linkMode) + val displayEnd = displayOffset + segDisplay.length + val overlapStart = maxOf(displayOffset, sel.first) + val overlapEnd = minOf(displayEnd, sel.last + 1) + if (overlapStart < overlapEnd) { /* emit */ } + displayOffset = displayEnd +} +``` + +Two changes only: + +1. **Seed with prefix length.** `var displayOffset = prefix.length` (or `itemPrefixText(ci).length` for `snapOffset`). Loop body is otherwise byte-for-byte identical to pre-PR. For non-reports `prefix.length == 0`, so the non-report path is unchanged. + +2. **Emit the prefix slice.** Before the loop, append the portion of `prefix` covered by the selection: + ```kotlin + if (sel.first < prefix.length) { + sb.append(prefix, sel.first, minOf(prefix.length, sel.last + 1)) + } + ``` + `selectedRange()` guarantees `sel.first ≥ 0`, so no clamping is needed at this site. + +3. **Handle the `formattedText == null` branch.** Reports with empty body have null `formattedText`, but the prefix selection still has to be returned. The early-return in the pre-PR null branch is replaced by the same `StringBuilder` path so prefix-only selections work: + ```kotlin + val formattedText = ci.formattedText ?: run { + val start = (sel.first - prefix.length).coerceAtLeast(0).coerceAtMost(ci.text.length) + val end = (sel.last + 1 - prefix.length).coerceAtMost(ci.text.length) + if (start < end) sb.append(ci.text, start, end) + return sb.toString() + } + ``` + `coerceAtLeast(0)` on `start` is required here because `sel.first - prefix.length` is negative when the selection lies entirely inside the prefix. + +### 3.3 Single source of truth + +Pre-PR, the prefix expression `if (mc.text.isEmpty()) mc.reason.text else "${mc.reason.text}: "` lived inline at two render sites: + +- `FramedItemView.kt:368` — the actual report row +- `ChatPreviewView.kt:262` — the chat list preview + +Re-introducing it inline in `TextSelection.kt` would have re-created exactly the silent coupling that produced the bug — a future change to the separator (e.g. localised colon) at the renderer would silently break copy/snap. The fix factors the expression into: + +```kotlin +// TextItemView.kt +fun itemPrefixText(ci: ChatItem): String = when (val mc = ci.content.msgContent) { + is MsgContent.MCReport -> if (mc.text.isEmpty()) mc.reason.text else "${mc.reason.text}: " + else -> "" +} +``` + +Both renderers and both selection-side functions now derive the string from this one definition. + +### 3.4 Edge cases verified + +| Case | Pre-PR | Post-PR | +|---|---|---| +| Non-report, fmt non-null (markdown) | works | byte-identical loop, works | +| Non-report, fmt null (plain text) | substring fast path | StringBuilder path, value-equivalent | +| Non-report, sel out of bounds | clamped to `[L, L]` → `""` | same | +| Report, full sel `Spam: test` | returns `test` (BUG) | returns `Spam: test` | +| Report, prefix-only sel | returns `""` (BUG) | returns prefix slice | +| Report, body-only sel | returns body (offset shift was hidden by `Int.MAX_VALUE` clamp at `sel.last+1`) | returns body | +| Report, sel.first == prefix.length | works coincidentally | works | +| Report, empty body, prefix-only sel | returns `""` | returns prefix | +| Report with mention having `localAlias` (transformed) | snap snapped to wrong char (BUG) | snaps correctly | +| Multi-item interior sel (`sel.last = MAX-1`) | works | no overflow on `+1 - prefix.length` | + +### 3.5 What was deliberately not done + +- **Performance restoration of the non-report null-fmt path.** Pre-PR returned `ci.text.substring(...)` directly (1 allocation). Post-PR uses `StringBuilder` (3 allocations). `selectedItemCopiedText` runs once per selected item per copy action — never on a hot path. Restoring the pre-PR fast path with an `if (prefix.isEmpty() && formattedText == null)` early return adds 4 lines of branching for negligible gain. Not worth it. + +- **Migrating `ChatPreviewView.kt` was kept** because it crossed the 3-site extraction threshold (FramedItemView + ChatPreviewView + 2× TextSelection) and the bug we are fixing is exactly the failure mode of duplicating this expression. ChatPreviewView is not a selection site, so no behaviour change — it shifts to the same single source of truth. + +## 4. Detailed implementation plan + +### 4.1 Files touched (final state) + +| File | Δ | Purpose | +|---|---|---| +| `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt` | +7 / 0 | new `itemPrefixText(ci)` helper next to `itemDisplayText` | +| `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt` | +1 / −1 | report branch delegates to `itemPrefixText(ci)` | +| `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt` | +2 / −2 | preview row delegates; drops unused `val mc =` | +| `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/TextSelection.kt` | +16 / −8 | the actual fix in `selectedItemCopiedText` and `snapOffset`, plus import | + +Total: 4 files, +26 / −11. + +### 4.2 Step-by-step (final commit `96d6f3222`) + +1. **Add `itemPrefixText(ci)`** in `TextItemView.kt` next to `itemDisplayText` / `itemSegmentDisplayText`. Returns `""` for non-reports. + +2. **`FramedItemView.kt:365-372`** (`MCReport` branch): replace inline expression with `append(itemPrefixText(ci))`. The surrounding `withStyle(SpanStyle(color = Red, italic))` is preserved — visual rendering unchanged. + +3. **`ChatPreviewView.kt:258-264`**: replace inline expression with `append(itemPrefixText(ci))`. Drop the now-unused `val mc =` from `when (val mc = ci.content.msgContent)` (the discriminator becomes `when (ci.content.msgContent)`). + +4. **`TextSelection.kt`**: + - Add `import chat.simplex.common.views.chat.item.itemPrefixText`. + - In `selectedItemCopiedText`: + - Compute `val prefix = itemPrefixText(ci)` and `val sb = StringBuilder()` first. + - Emit prefix slice if `sel.first < prefix.length`. + - Modify the `formattedText ?: ...` early-return to a `?: run { … }` block that adds the body slice (offsets shifted by `-prefix.length`, clamped) to `sb` and returns `sb.toString()`. + - Seed the formattedText loop with `var displayOffset = prefix.length`. Loop body unchanged. + - In `snapOffset`: change `var displayOffset = 0` to `var displayOffset = itemPrefixText(ci).length`. Loop body unchanged. + - Update the docstring on `selectedItemCopiedText` to note that display-text space includes any leading `itemPrefixText`. + +### 4.3 Verification + +- `./gradlew :common:compileKotlinDesktop` — passes (warnings are pre-existing). +- `bash /home/user/build/linux.sh` — full Linux x86_64 AppImage produced (`SimpleX_Chat-x86_64-fix-select-in-reports.AppImage`). +- Manual test plan, all in desktop: + 1. Open a chat with a report whose rendered form is `Spam: test`. Select across the whole line + Ctrl-C → clipboard reads `Spam: test`. + 2. Select only the red prefix → clipboard reads the prefix. + 3. Select only the comment → clipboard reads the comment. + 4. Report comment containing `@alice (Bob)` (mention with localAlias). Drag a selection boundary into the mention → on release, highlight snaps to mention boundaries. + 5. Plain (non-report) messages: full-line, partial, mention, link selections — clipboard contents unchanged from pre-PR. + 6. Multi-item selection across non-report and report rows — prefixes appear inline at the correct positions. + +### 4.4 Risk and rollback + +- **Blast radius** is the desktop selection-copy code path. iOS / Android use separate selection mechanisms and are unaffected. +- The non-report selection path's inner loop body is byte-for-byte identical to pre-PR (the `displayOffset = 0` initialisation is unchanged when `prefix.length == 0`), so regressions on non-reports would require the prefix expression itself to fail — which is impossible because `itemPrefixText` returns `""` for any `msgContent` other than `MCReport`. +- Rollback is `git revert 96d6f3222 e97dd7bf4 6aacfa4d2` (three commits) and a force-push, restoring the pre-PR copy behaviour with the original bug. + +## 5. Why this specific shape + +- Recognising the prefix as the *first* display-space segment turns the bug into a one-line seed change. No special-cased report branch in copy/snap; the existing loop handles both. +- The inner loop of `selectedItemCopiedText` and `snapOffset` is byte-for-byte identical to pre-PR. Only the seed value of `displayOffset` and the pre-/post-amble change. +- Four sites need the prefix string (FramedItemView, ChatPreviewView, and two in TextSelection). `itemPrefixText` becomes their single point of change, closing the silent-coupling gap that produced the bug. +- `selectedRange()` guarantees `sel.first ≥ 0`, so no `coerceAtLeast(0)` is added at the prefix-slice append. The one `coerceAtLeast(0)` that survives (on the `formattedText == null` body branch) is reachable when the selection lies entirely inside the prefix and is needed. +- Final PR is 4 files, +26 / −11. The inner loop body changes by zero lines. diff --git a/scripts/android/build-android-bundle.sh b/scripts/android/build-android-bundle.sh index 554e64edff..b784da2aad 100755 --- a/scripts/android/build-android-bundle.sh +++ b/scripts/android/build-android-bundle.sh @@ -23,5 +23,5 @@ unzip -o "$tmp/libsimplex.zip" -d "$tmp/simplex-chat/apps/multiplatform/common/s curl -sSf "$libsup" -o "$tmp/libsupport.zip" unzip -o "$tmp/libsupport.zip" -d "$tmp/simplex-chat/apps/multiplatform/common/src/commonMain/cpp/android/libs/arm64-v8a" -gradle -p "$tmp/simplex-chat/apps/multiplatform/" clean build +gradle -p "$tmp/simplex-chat/apps/multiplatform/" -Psimplex.assets.dir=../../assets clean build cp "$tmp/simplex-chat/apps/multiplatform/android/build/outputs/apk/release/android-release-unsigned.apk" "$PWD/simplex-chat.apk" diff --git a/scripts/android/build-android.sh b/scripts/android/build-android.sh index afd13011c9..7edee9c304 100755 --- a/scripts/android/build-android.sh +++ b/scripts/android/build-android.sh @@ -67,13 +67,13 @@ checks() { if ! command -v "$i" > /dev/null 2>&1; then commands_failed="$i $commands_failed" else - gradle_ver_local="$(gradle -v | grep Gradle | awk '{print $2}')" - gradle_ver_local_compare="$(printf ${gradle_ver_local:-0.0} | awk -F. '{print $1$2}')" + gradle_ver_local="$(gradle --version | sed -n 's/^Gradle //p')" + gradle_ver_local_compare="$(printf '%s' "$gradle_ver_local" | awk -F. '{print $1"."$2}')" gradle_ver_remote="$(grep distributionUrl ${folder}/apps/multiplatform/gradle/wrapper/gradle-wrapper.properties)" gradle_ver_remote="${gradle_ver_remote#*-}" gradle_ver_remote="${gradle_ver_remote%-*}" - gradle_ver_remote_compare="$(printf ${gradle_ver_remote} | awk -F. '{print $1$2}')" - + gradle_ver_remote_compare="$(printf '%s' "$gradle_ver_remote" | awk -F. '{print $1"."$2}')" + if [ "$gradle_ver_local_compare" != "$gradle_ver_remote_compare" ]; then commands_failed="$i[installed=${gradle_ver_local},required=${gradle_ver_remote}] $commands_failed" fi @@ -134,7 +134,7 @@ build() { # Build only one arch sed -i.bak "s/include(.*/include(\"${android_arch}\")/" "$folder/apps/multiplatform/android/build.gradle.kts" - gradle -p "$folder/apps/multiplatform/" clean :android:assembleRelease + gradle -p "$folder/apps/multiplatform/" -Psimplex.assets.dir=../../assets clean :android:assembleRelease mkdir -p "$android_tmp_folder" unzip -oqd "$android_tmp_folder" "$android_apk_output" diff --git a/scripts/ci/build-desktop-mac.sh b/scripts/ci/build-desktop-mac.sh index 9adea013b4..044d407b3a 100755 --- a/scripts/ci/build-desktop-mac.sh +++ b/scripts/ci/build-desktop-mac.sh @@ -16,5 +16,10 @@ security unlock-keychain -p "" /tmp/simplex.keychain security list-keychains -s `security list-keychains | xargs` /tmp/simplex.keychain scripts/desktop/build-lib-mac.sh cd apps/multiplatform -./gradlew packageDmg -./gradlew notarizeDmg +if [ -n "${ASSETS_DIR:-}" ]; then + set -- -Psimplex.assets.dir="$ASSETS_DIR" +else + set -- +fi +./gradlew "$@" packageDmg +./gradlew "$@" notarizeDmg diff --git a/scripts/desktop/build-lib-linux.sh b/scripts/desktop/build-lib-linux.sh index 7868a125b6..a2684b87d2 100755 --- a/scripts/desktop/build-lib-linux.sh +++ b/scripts/desktop/build-lib-linux.sh @@ -8,6 +8,7 @@ function readlink() { OS=linux ARCH="$(uname -m)" +DATABASE_BACKEND="${1:-sqlite}" GHC_VERSION=9.6.3 if [ "$ARCH" == "aarch64" ]; then @@ -25,7 +26,13 @@ for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done #rm -rf $BUILD_DIR -cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -optl-Wl,-soname,libsimplex.so -flink-rts -threaded' --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' +if [[ "$DATABASE_BACKEND" == "postgres" ]]; then + echo "Building with postgres backend..." + cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -optl-Wl,-soname,libsimplex.so -flink-rts -threaded' --constraint 'simplexmq +client_library +client_postgres' --constraint 'simplex-chat +client_library +client_postgres' +else + echo "Building with sqlite backend..." + cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -optl-Wl,-soname,libsimplex.so -flink-rts -threaded' --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' +fi cd $BUILD_DIR/build mv libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so libsimplex.so 2> /dev/null || true #patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libsimplex.so diff --git a/scripts/desktop/make-appimage-linux.sh b/scripts/desktop/make-appimage-linux.sh index c242b63d54..650c98d522 100755 --- a/scripts/desktop/make-appimage-linux.sh +++ b/scripts/desktop/make-appimage-linux.sh @@ -18,7 +18,12 @@ libcrypto_path=$(ldd common/src/commonMain/cpp/desktop/libs/*/libHSdirect-sqlcip trap "rm common/src/commonMain/cpp/desktop/libs/*/`basename $libcrypto_path` 2> /dev/null || true" EXIT cp $libcrypto_path common/src/commonMain/cpp/desktop/libs/* -./gradlew createDistributable +if [ -n "${ASSETS_DIR:-}" ]; then + set -- -Psimplex.assets.dir="$ASSETS_DIR" +else + set -- +fi +./gradlew "$@" createDistributable rm common/src/commonMain/cpp/desktop/libs/*/`basename $libcrypto_path` rm -rf $release_app_dir/AppDir 2>/dev/null diff --git a/scripts/desktop/make-deb-linux.sh b/scripts/desktop/make-deb-linux.sh index 3226c22709..0fa543a81d 100755 --- a/scripts/desktop/make-deb-linux.sh +++ b/scripts/desktop/make-deb-linux.sh @@ -4,7 +4,12 @@ ARCH="$(uname -m)" scripts/desktop/build-lib-linux.sh cd apps/multiplatform -./gradlew packageDeb +if [ -n "${ASSETS_DIR:-}" ]; then + set -- -Psimplex.assets.dir="$ASSETS_DIR" +else + set -- +fi +./gradlew "$@" packageDeb # Workaround for skiko library # diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index 0d628c1c67..a2bafe8a70 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,55 @@ + + https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html + +

New in v6.5.1:

+
    +
  • additional preset chat relay.
  • +
  • fixed a rare bug when receiving files.
  • +
+

New in v6.5:

+

Public channels - speak freely!

+
    +
  • Reliability: many relays per channel.
  • +
  • Ownership: you can run your own relays.
  • +
  • Security: owners hold channel keys.
  • +
  • Privacy: for owners and subscribers.
  • +
+

Easier to invite your friends: we made connecting simpler for new users.

+

Safe web links:

+
    +
  • opt-in to send link previews.
  • +
  • use SOCKS proxy for previews (if enabled).
  • +
  • prevent hyperlink phishing.
  • +
  • remove link tracking.
  • +
+

Non-profit governance: to make SimpleX Network last.

+
+
+ + https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html + +

New in v6.5.

+

Public channels - speak freely!

+
    +
  • Reliability: many relays per channel.
  • +
  • Ownership: you can run your own relays.
  • +
  • Security: owners hold channel keys.
  • +
  • Privacy: for owners and subscribers.
  • +
+

Easier to invite your friends: we made connecting simpler for new users.

+

Safe web links:

+
    +
  • opt-in to send link previews.
  • +
  • use SOCKS proxy for previews (if enabled).
  • +
  • prevent hyperlink phishing.
  • +
  • remove link tracking.
  • +
+

Non-profit governance: to make SimpleX Network last.

+
+
https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html diff --git a/scripts/ios/copy-assets.sh b/scripts/ios/copy-assets.sh new file mode 100755 index 0000000000..f29ebd2464 --- /dev/null +++ b/scripts/ios/copy-assets.sh @@ -0,0 +1,50 @@ +#!/bin/sh +set -eu + +# Copies generated iOS assets into SimpleXAssets.xcassets. +# Intended to run as an Xcode Run Script build phase. +# Skips silently if SIMPLEX_ASSETS is not in SWIFT_ACTIVE_COMPILATION_CONDITIONS +# or if the source directory is not found. +# +# The source path is resolved in order: +# 1. Command-line argument +# 2. SIMPLEX_ASSETS_DIR build setting (set in Local.xcconfig) +# 3. No default — skips if neither is set +# +# Manual usage: ./scripts/copy-assets.sh path/to/assets + +# Skip if SIMPLEX_ASSETS flag is not set (unless run manually outside Xcode) +if [ -n "${SWIFT_ACTIVE_COMPILATION_CONDITIONS:-}" ]; then + case " $SWIFT_ACTIVE_COMPILATION_CONDITIONS " in + *" SIMPLEX_ASSETS "*) ;; + *) exit 0 ;; + esac +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +IOS_DIR="$SCRIPT_DIR/../../apps/ios/Shared/SimpleXAssets.xcassets" + +ASSETS_ROOT="${1:-${SIMPLEX_ASSETS_DIR:-}}" +if [ -z "$ASSETS_ROOT" ]; then + echo "warning: SIMPLEX_ASSETS_DIR not set and no path argument provided" >&2 + exit 0 +fi + +SRC_DIR="$ASSETS_ROOT/ios/Assets.xcassets" + +if [ ! -d "$SRC_DIR" ]; then + echo "warning: source assets not found: $SRC_DIR (run resize.sh first)" >&2 + exit 0 +fi + +# Remove old imagesets but keep root Contents.json +find "$IOS_DIR" -name "*.imageset" -type d -exec rm -rf {} + 2>/dev/null || true + +# Copy imagesets +for imageset in "$SRC_DIR"/*.imageset; do + [ -d "$imageset" ] || continue + cp -r "$imageset" "$IOS_DIR/" + echo "Copied $(basename "$imageset")" +done + +echo "Done. Assets copied to $IOS_DIR" diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 44af955f40..feee62af4a 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."99f9de71e5df213bb062fa11dd165778fc1d7160" = "1gqza72i1lnllj0h0i6d2mf47zy1k4yinvmm61wypij5n1pf626h"; + "https://github.com/simplex-chat/simplexmq.git"."8bd3193280da6b4decf790bb57b470780c2576ba" = "0zzrsdgsqdiff6ks7l4qsik9p5023f1n56iaf7x395l4ykf6bkqm"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/scripts/simplex-chat-reproduce-builds.sh b/scripts/simplex-chat-reproduce-builds.sh index b05d41efbc..d512735bf1 100755 --- a/scripts/simplex-chat-reproduce-builds.sh +++ b/scripts/simplex-chat-reproduce-builds.sh @@ -110,7 +110,7 @@ for os_pair in ${oses}; do # Desktop: deb docker exec \ -t "${container_name}" \ - sh -c './scripts/desktop/make-deb-linux.sh' + sh -c "export ASSETS_DIR='../../assets'; ./scripts/desktop/make-deb-linux.sh" # Copy deb docker cp \ @@ -128,7 +128,7 @@ for os_pair in ${oses}; do # Appimage docker exec \ -t "${container_name}" \ - sh -c './scripts/desktop/make-appimage-linux.sh && mv ./apps/multiplatform/release/main/*imple*.AppImage ./apps/multiplatform/release/main/simplex.appimage' + sh -c "export ASSETS_DIR='../../assets'; ./scripts/desktop/make-appimage-linux.sh && mv ./apps/multiplatform/release/main/*imple*.AppImage ./apps/multiplatform/release/main/simplex.appimage" # Copy appimage docker cp \ diff --git a/simplex-chat.cabal b/simplex-chat.cabal index e6bbbf38d8..6c343164fc 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.5.0.12 +version: 6.5.1.1 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat @@ -130,7 +130,9 @@ library Simplex.Chat.Store.Postgres.Migrations.M20260122_has_link Simplex.Chat.Store.Postgres.Migrations.M20260222_chat_relays Simplex.Chat.Store.Postgres.Migrations.M20260403_item_viewed - Simplex.Chat.Store.Postgres.Migrations.M20260407_client_services + Simplex.Chat.Store.Postgres.Migrations.M20260429_relay_request_retries + Simplex.Chat.Store.Postgres.Migrations.M20260507_relay_inactive_at + Simplex.Chat.Store.Postgres.Migrations.M20260520_client_services else exposed-modules: Simplex.Chat.Archive @@ -283,7 +285,9 @@ library Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link Simplex.Chat.Store.SQLite.Migrations.M20260222_chat_relays Simplex.Chat.Store.SQLite.Migrations.M20260403_item_viewed - Simplex.Chat.Store.SQLite.Migrations.M20260407_client_services + Simplex.Chat.Store.SQLite.Migrations.M20260429_relay_request_retries + Simplex.Chat.Store.SQLite.Migrations.M20260507_relay_inactive_at + Simplex.Chat.Store.SQLite.Migrations.M20260520_client_services other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 448a2bbc16..ec17614db3 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -10,6 +10,7 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE TupleSections #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} @@ -26,7 +27,7 @@ import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, mapMaybe) import Data.Text (Text) -import Data.Time.Clock (getCurrentTime) +import Data.Time.Clock (getCurrentTime, nominalDay) import Simplex.Chat.Controller import Simplex.Chat.Library.Commands import Simplex.Chat.Operators @@ -42,6 +43,7 @@ import Simplex.Chat.Util (shuffle) import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), allRoles, createAgentStore, defaultAgentConfig, presetServerCfg) +import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..)) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.Common (DBStore (dbNew)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -115,6 +117,10 @@ defaultChatConfig = deliveryWorkerDelay = 0, deliveryBucketSize = 10000, channelSubscriberRole = GRObserver, + relayChecksInterval = 15 * 60, -- 15 minutes + relayInactiveTTL = nominalDay, + relayRequestRetryInterval = RetryInterval {initialInterval = 5_000000, increaseAfter = 0, maxInterval = 600_000000}, + relayRequestExpiry = (10, nominalDay), deviceNameForRemote = "", remoteCompression = True, chatHooks = defaultChatHooks diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 537c423423..8e6fac5d10 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -73,6 +73,7 @@ import Simplex.Messaging.Agent (AgentClient, DatabaseDiff, SubscriptionsInfo) import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg, Worker) import Simplex.Messaging.Agent.Lock +import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..)) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction, withTransactionPriority) import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation, UpMigration) @@ -158,6 +159,10 @@ data ChatConfig = ChatConfig deliveryWorkerDelay :: Int64, -- microseconds deliveryBucketSize :: Int, channelSubscriberRole :: GroupMemberRole, -- TODO [relays] starting role should be communicated in protocol from owner to relays + relayChecksInterval :: NominalDiffTime, + relayInactiveTTL :: NominalDiffTime, + relayRequestRetryInterval :: RetryInterval, + relayRequestExpiry :: (Int, NominalDiffTime), highlyAvailable :: Bool, deviceNameForRemote :: Text, remoteCompression :: Bool, @@ -342,6 +347,7 @@ data ChatCommand | APIGetReactionMembers {userId :: UserId, groupId :: GroupId, chatItemId :: ChatItemId, reaction :: MsgReaction} | APIPlanForwardChatItems {fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId} | APIForwardChatItems {toChatRef :: ChatRef, sendAsGroup :: ShowGroupAsSender, fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId, ttl :: Maybe Int} + | APIShareChatMsgContent {shareChatRef :: ChatRef, toSendRef :: SendRef} | APIUserRead UserId | UserRead | APIChatRead {chatRef :: ChatRef} @@ -470,13 +476,13 @@ data ChatCommand | AddContact IncognitoEnabled | APISetConnectionIncognito Int64 IncognitoEnabled | APIChangeConnectionUser Int64 UserId -- new user id to switch connection to - | APIConnectPlan {userId :: UserId, connectionLink :: Maybe AConnectionLink} -- Maybe is used to report link parsing failure as special error + | APIConnectPlan {userId :: UserId, connectionLink :: Maybe AConnectionLink, resolveKnown :: Bool, linkOwnerSig :: Maybe LinkOwnerSig} -- Maybe AConnectionLink is used to report link parsing failure as special error | APIPrepareContact UserId ACreatedConnLink ContactShortLinkData | APIPrepareGroup UserId CreatedLinkContact DirectLink GroupShortLinkData | APIChangePreparedContactUser ContactId UserId | APIChangePreparedGroupUser GroupId UserId | APIConnectPreparedContact {contactId :: ContactId, incognito :: IncognitoEnabled, msgContent_ :: Maybe MsgContent} - | APIConnectPreparedGroup GroupId IncognitoEnabled (Maybe MsgContent) + | APIConnectPreparedGroup {groupId :: GroupId, incognito :: IncognitoEnabled, ownerContact :: Maybe GroupOwnerContact, msgContent_ :: Maybe MsgContent} | APIConnect {userId :: UserId, incognito :: IncognitoEnabled, preparedLink_ :: Maybe ACreatedConnLink} -- Maybe is used to report link parsing failure as special error | Connect {incognito :: IncognitoEnabled, connLink_ :: Maybe AConnectionLink} | APIConnectContactViaAddress UserId IncognitoEnabled ContactId @@ -501,6 +507,7 @@ data ChatCommand | ForwardMessage {toChatName :: ChatName, fromContactName :: ContactName, forwardedMsg :: Text} | ForwardGroupMessage {toChatName :: ChatName, fromGroupName :: GroupName, fromMemberName_ :: Maybe ContactName, forwardedMsg :: Text} | ForwardLocalMessage {toChatName :: ChatName, forwardedMsg :: Text} + | SharePublicGroup {shareGroupName :: GroupName, toChatName :: ChatName} | SendMessage SendName Text | SendMemberContactMessage GroupName ContactName Text | AcceptMemberContact ContactName @@ -517,6 +524,7 @@ data ChatCommand -- TODO [relays] starting role should be communicated in protocol from owner to relays (see channelSubscriberRole config) | APINewPublicGroup {userId :: UserId, incognito :: IncognitoEnabled, relayIds :: NonEmpty Int64, groupProfile :: GroupProfile} | APIGetGroupRelays {groupId :: GroupId} + | APIAddGroupRelays {groupId :: GroupId, relayIds :: NonEmpty Int64} | NewPublicGroup IncognitoEnabled (NonEmpty Int64) GroupProfile | AddMember GroupName ContactName GroupMemberRole | JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter} @@ -650,6 +658,12 @@ data RelayConnectionResult = RelayConnectionResult } deriving (Show) +data AddRelayResult = AddRelayResult + { relay :: UserChatRelay, + relayError :: Maybe ChatError + } + deriving (Show) + data RelayTestStep = RTSGetLink | RTSDecodeLink @@ -720,7 +734,10 @@ data ChatResponse | CRWelcome {user :: User} | CRGroupCreated {user :: User, groupInfo :: GroupInfo} | CRPublicGroupCreated {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay]} + | CRPublicGroupCreationFailed {user :: User, addRelayResults :: [AddRelayResult]} | CRGroupRelays {user :: User, groupInfo :: GroupInfo, groupRelays :: [GroupRelay]} + | CRGroupRelaysAdded {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay]} + | CRGroupRelaysAddFailed {user :: User, addRelayResults :: [AddRelayResult]} | CRGroupMembers {user :: User, group :: Group} | CRMemberSupportChats {user :: User, groupInfo :: GroupInfo, members :: [GroupMember]} -- | CRGroupConversationsArchived {user :: User, groupInfo :: GroupInfo, archivedGroupConversations :: [GroupConversation]} @@ -761,6 +778,7 @@ data ChatResponse | CRLeftMemberUser {user :: User, groupInfo :: GroupInfo} | CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo, msgSigned :: Bool} | CRForwardPlan {user :: User, itemsCount :: Int, chatItemIds :: [ChatItemId], forwardConfirmation :: Maybe ForwardConfirmation} + | CRChatMsgContent {user :: User, msgContent :: MsgContent} | CRRcvFileAccepted {user :: User, chatItem :: AChatItem} -- TODO add chatItem :: AChatItem | CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer} @@ -982,7 +1000,7 @@ data ChatPagination deriving (Show) data PaginationByTime - = PTLast Int + = PTLast {count :: Int} | PTAfter UTCTime Int | PTBefore UTCTime Int deriving (Show) @@ -1009,14 +1027,14 @@ data ConnectionPlan deriving (Show) data InvitationLinkPlan - = ILPOk {contactSLinkData_ :: Maybe ContactShortLinkData} + = ILPOk {contactSLinkData_ :: Maybe ContactShortLinkData, ownerVerification :: Maybe OwnerVerification} | ILPOwnLink | ILPConnecting {contact_ :: Maybe Contact} | ILPKnown {contact :: Contact} deriving (Show) data ContactAddressPlan - = CAPOk {contactSLinkData_ :: Maybe ContactShortLinkData} + = CAPOk {contactSLinkData_ :: Maybe ContactShortLinkData, ownerVerification :: Maybe OwnerVerification} | CAPOwnLink | CAPConnectingConfirmReconnect | CAPConnectingProhibit {contact :: Contact} @@ -1025,11 +1043,29 @@ data ContactAddressPlan deriving (Show) data GroupLinkPlan - = GLPOk {groupSLinkInfo_ :: Maybe GroupShortLinkInfo, groupSLinkData_ :: Maybe GroupShortLinkData} + = GLPOk {groupSLinkInfo_ :: Maybe GroupShortLinkInfo, groupSLinkData_ :: Maybe GroupShortLinkData, ownerVerification :: Maybe OwnerVerification} | GLPOwnLink {groupInfo :: GroupInfo} | GLPConnectingConfirmReconnect | GLPConnectingProhibit {groupInfo_ :: Maybe GroupInfo} - | GLPKnown {groupInfo :: GroupInfo} + | GLPKnown {groupInfo :: GroupInfo, groupUpdated :: BoolDef, ownerVerification :: Maybe OwnerVerification, linkOwners :: ListDef GroupLinkOwner} + | GLPNoRelays {groupSLinkData_ :: Maybe GroupShortLinkData} + deriving (Show) + +data GroupLinkOwner = GroupLinkOwner + { memberId :: MemberId, + memberKey :: C.PublicKeyEd25519 + } + deriving (Show) + +data OwnerVerification + = OVVerified + | OVFailed {reason :: Text} + deriving (Show) + +data GroupOwnerContact = GroupOwnerContact + { contactId :: ContactId, + memberId :: MemberId + } deriving (Show) type DirectLink = Bool @@ -1044,11 +1080,11 @@ data GroupShortLinkInfo = GroupShortLinkInfo connectionPlanProceed :: ConnectionPlan -> Bool connectionPlanProceed = \case CPInvitationLink ilp -> case ilp of - ILPOk _ -> True + ILPOk {} -> True ILPOwnLink -> True _ -> False CPContactAddress cap -> case cap of - CAPOk _ -> True + CAPOk {} -> True CAPOwnLink -> True CAPConnectingConfirmReconnect -> True CAPContactViaAddress _ -> True @@ -1057,6 +1093,7 @@ connectionPlanProceed = \case GLPOk {} -> True GLPOwnLink _ -> True GLPConnectingConfirmReconnect -> True + GLPNoRelays _ -> False _ -> False CPError _ -> True @@ -1642,12 +1679,16 @@ $(JQ.deriveJSON (enumJSON $ dropPrefix "HS") ''HelpSection) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CLQ") ''ChatListQuery) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "OV") ''OwnerVerification) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "ILP") ''InvitationLinkPlan) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CAP") ''ContactAddressPlan) $(JQ.deriveJSON defaultJSON ''GroupShortLinkInfo) +$(JQ.deriveJSON defaultJSON ''GroupLinkOwner) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GLP") ''GroupLinkPlan) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FC") ''ForwardConfirmation) @@ -1714,6 +1755,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "TE") ''TerminalEvent) $(JQ.deriveJSON defaultJSON ''RelayConnectionResult) +$(JQ.deriveJSON defaultJSON ''AddRelayResult) + $(JQ.deriveJSON (enumJSON $ dropPrefix "RTS") ''RelayTestStep) $(JQ.deriveJSON defaultJSON ''RelayTestFailure) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index cf11787a54..81c230e88d 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -37,6 +37,7 @@ import Data.Constraint (Dict (..)) import Data.Either (fromRight, partitionEithers, rights) import Data.Foldable (foldr') import Data.Functor (($>)) +import Data.Functor.Identity (Identity (..), runIdentity) import Data.Int (Int64) import Data.List (dropWhileEnd, find, foldl', isSuffixOf, partition, sortOn, zipWith4) import Data.List.NonEmpty (NonEmpty (..)) @@ -56,9 +57,11 @@ import qualified Data.UUID.V4 as V4 import Simplex.Chat.Library.Subscriber import Simplex.Chat.Call import Simplex.Chat.Controller +import Simplex.Chat.Delivery (DeliveryJobScope (..), DeliveryJobSpec (..), DeliveryWorkerScope (..)) import Simplex.Chat.Files import Simplex.Chat.Markdown import Simplex.Chat.Messages +import Simplex.Chat.Messages.Batch (encodeBatchElement) import Simplex.Chat.Messages.CIContent import Simplex.Chat.Messages.CIContent.Events import Simplex.Chat.Operators @@ -101,6 +104,7 @@ import qualified Simplex.Messaging.Crypto.ShortLink as SL import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQSupportOff, pattern PQSupportOn) +import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (base64P) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) @@ -635,7 +639,10 @@ processChatCommand vr nm = \case mapM_ assertNoMentions cms withContactLock "sendMessage" chatId $ sendContactContentMessages user chatId live itemTTL (L.map composedMessageReq cms) - SRGroup chatId gsScope asGroup -> + SRGroup chatId gsScope asGroup -> do + case gsScope of + Just (GCSMemberSupport _) -> when asGroup $ throwCmdError "cannot send as group in support scope" + Nothing -> pure () withGroupLock "sendMessage" chatId $ do (gInfo, cmrs) <- withFastStore $ \db -> do g <- getGroupInfo db vr user chatId @@ -704,7 +711,7 @@ processChatCommand vr nm = \case gInfo@GroupInfo {groupId, membership} <- withFastStore $ \db -> getGroupInfo db vr user chatId when (isNothing scope) $ assertUserGroupRole gInfo GRAuthor let (_, ft_) = msgContentTexts mc - if prohibitedSimplexLinks gInfo membership ft_ + if prohibitedSimplexLinks gInfo membership mc ft_ then throwCmdError ("feature not allowed " <> T.unpack (groupFeatureNameText GFSimplexLinks)) else do -- TODO [knocking] check chat item scope? @@ -1013,7 +1020,13 @@ processChatCommand vr nm = \case CTContactConnection -> throwCmdError "not supported" where prepareMsgReq :: CChatItem c -> CM (Maybe (MsgContent, Maybe CryptoFile)) - prepareMsgReq (CChatItem _ ci) = forwardMsgContent ci $>>= forwardContent ci + prepareMsgReq (CChatItem md ci) = forwardMsgContent ci $>>= forwardContent ci . dropOwnerSig + where + dropOwnerSig = \case + mc@MCChat {text, chatLink} + | SMDSnd <- md, fromChat == toChat -> mc + | otherwise -> MCChat {text, chatLink, ownerSig = Nothing} + mc -> mc forwardCIFF :: ChatItem c d -> Maybe CIForwardedFrom -> Maybe CIForwardedFrom forwardCIFF ChatItem {meta = CIMeta {itemForwarded}} ciff = case itemForwarded of Nothing -> ciff @@ -1081,6 +1094,41 @@ processChatCommand vr nm = \case let formattedDate = formatTime defaultTimeLocale "%Y%m%d_%H%M%S" currentDate let ext = takeExtension fileName pure $ prefix <> formattedDate <> ext + APIShareChatMsgContent (ChatRef CTGroup groupId _) toSendRef -> withUser $ \user -> do + GroupInfo {groupProfile = gp@GroupProfile {publicGroup}, membership = GroupMember {memberId, memberRole}, groupKeys} <- + withFastStore $ \db -> getGroupInfo db vr user groupId + case publicGroup of + Nothing -> throwCmdError "not a public group" + Just PublicGroupProfile {groupLink} -> do + let signingKeys = case (memberRole, groupKeys) of + (GROwner, Just gk@GroupKeys {groupRootKey = GRKPrivate _}) -> Just gk + _ -> Nothing + ownerSig <- + pure signingKeys $>>= \GroupKeys {memberPrivKey} -> + mkLinkOwnerSig memberPrivKey groupLink memberId <$$> shareChatBinding user toSendRef + let text = safeDecodeUtf8 $ strEncode groupLink + pure $ CRChatMsgContent user MCChat {text, chatLink = MCLGroup groupLink gp, ownerSig} + where + mkLinkOwnerSig :: ConnectionModeI m => C.PrivateKeyEd25519 -> ConnShortLink m -> MemberId -> (ChatBinding, ByteString) -> LinkOwnerSig + mkLinkOwnerSig privKey connLink MemberId {unMemberId} (cbTag, bindingData) = + let ownerId = Just $ B64UrlByteString unMemberId + cb = encodeChatBinding cbTag bindingData + ownerSig = C.sign' privKey $ cb <> smpEncode connLink + in LinkOwnerSig {ownerId, chatBinding = B64UrlByteString cb, ownerSig} + shareChatBinding :: User -> SendRef -> CM (Maybe (ChatBinding, ByteString)) + shareChatBinding u = \case + SRDirect contactId -> do + ct <- withFastStore $ \db -> getContact db vr u contactId + forM (contactConn ct) $ \conn -> + (CBDirect,) <$> withAgent (`getConnectionRatchetAdHash` aConnId conn) + SRGroup toGroupId _ asGroup -> do + GroupInfo {groupProfile = GroupProfile {publicGroup}, membership = m} <- withFastStore $ \db -> getGroupInfo db vr u toGroupId + pure $ mkBinding m <$> publicGroup + where + mkBinding GroupMember {memberId} PublicGroupProfile {publicGroupId = pgId} + | asGroup = (CBChannel, smpEncode pgId) + | otherwise = (CBGroup, smpEncode (pgId, memberId)) + APIShareChatMsgContent _ _ -> throwCmdError "sharing is only supported for public groups" APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user UserRead -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUserRead userId APIChatRead chatRef@(ChatRef cType chatId scope_) -> withUser $ \_ -> case cType of @@ -1295,7 +1343,7 @@ processChatCommand vr nm = \case Just smId -> void $ sendDirectContactMessage user ct $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing Nothing Nothing -> do - (msg, _) <- sendDirectContactMessage user ct $ XMsgNew $ MCSimple $ extMsgContent mc Nothing + (msg, _) <- sendDirectContactMessage user ct $ XMsgNew $ mcSimple mc ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] APIRejectContact connReqId -> withUser $ \user -> do @@ -1741,15 +1789,17 @@ processChatCommand vr nm = \case APIGroupInfo gId -> withUser $ \user -> CRGroupInfo user <$> withFastStore (\db -> getGroupInfo db vr user gId) APIGetUpdatedGroupLinkData groupId -> withUser $ \user -> do - gInfo@GroupInfo {groupProfile = GroupProfile {publicGroup}} <- withFastStore $ \db -> getGroupInfo db vr user groupId - case publicGroup of - Just PublicGroupProfile {groupLink = sLnk} | useRelays' gInfo -> do - (_, cData) <- getShortLinkConnReq nm user sLnk + gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} <- withFastStore $ \db -> getGroupInfo db vr user groupId + case p of + GroupProfile {publicGroup = Just PublicGroupProfile {groupLink = sLnk}} | useRelays' gInfo -> do + (_, cData@(ContactLinkData _ UserContactData {relays = currentRelayLinks})) <- getShortLinkConnReq' nm user sLnk groupSLinkData_ <- liftIO $ decodeLinkUserData cData - let publicGroupData_ = groupSLinkData_ >>= \GroupShortLinkData {publicGroupData} -> publicGroupData - publicMemberCount_ = (\PublicGroupData {publicMemberCount} -> publicMemberCount) <$> publicGroupData_ - gInfo' <- fromMaybe gInfo - <$> forM publicMemberCount_ (\count -> withFastStore $ \db -> setPublicMemberCount db vr user gInfo count) + gInfo' <- case groupSLinkData_ of + Just sLinkData -> fst <$> updateGroupFromLinkData user gInfo sLinkData + _ -> pure gInfo + when (memberRole' (membership gInfo) /= GROwner && memberCurrent (membership gInfo)) $ + withGroupLock "syncSubscriberRelays" groupId $ + syncSubscriberRelays user gInfo' currentRelayLinks pure $ CRGroupInfo user gInfo' _ -> throwCmdError "group link data not available" APIGroupMemberInfo gId gMemberId -> withUser $ \user -> do @@ -1932,7 +1982,7 @@ processChatCommand vr nm = \case where recreateConn user conn@PendingContactConnection {customUserProfileId, connLinkInv} newUser = do subMode <- chatReadVar subscriptionMode - let short = isJust $ connShortLink =<< connLinkInv + let short = isJust $ connShortLink' =<< connLinkInv userLinkData_ | short = Just $ UserInvLinkData $ contactShortLinkData (userProfileDirect newUser Nothing Nothing True) Nothing | otherwise = Nothing @@ -1945,9 +1995,9 @@ processChatCommand vr nm = \case createDirectConnection db newUser agConnId ccLink' Nothing ConnNew Nothing subMode initialChatVersion PQSupportOn deleteAgentConnectionAsync (aConnId' conn) pure conn' - APIConnectPlan userId (Just cLink) -> withUserId userId $ \user -> - uncurry (CRConnectionPlan user) <$> connectPlan user cLink - APIConnectPlan _ Nothing -> throwChatError CEInvalidConnReq + APIConnectPlan userId (Just cLink) resolveKnown linkOwnerSig_ -> withUserId userId $ \user -> + uncurry (CRConnectionPlan user) <$> connectPlan user cLink resolveKnown linkOwnerSig_ + APIConnectPlan _ Nothing _ _ -> throwChatError CEInvalidConnReq APIPrepareContact userId accLink contactSLinkData -> withUserId userId $ \user -> do let ContactShortLinkData {profile, message, business} = contactSLinkData welcomeSharedMsgId <- forM message $ \_ -> getSharedMsgId @@ -1976,7 +2026,7 @@ processChatCommand vr nm = \case let cd = CDDirectRcv ct createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing cInfo = DirectChat ct - void $ createItem Nothing $ CIRcvDirectE2EEInfo $ E2EInfo $ connRequestPQEncryption cReq + void $ createItem Nothing $ CIRcvDirectE2EEInfo $ e2eInfoEncrypted $ connRequestPQEncryption cReq void $ createFeatureEnabledItems_ user ct aci <- mapM (createItem welcomeSharedMsgId . CIRcvMsgContent) message let chat = case aci of @@ -2038,7 +2088,7 @@ processChatCommand vr nm = \case -- create changed feature items (connecting incognito sends default preferences, instead of user preferences) lift . when incognito $ createContactChangedFeatureItems user ct ct' forM_ msgContent_ $ \mc -> do - let evt = XMsgNew $ MCSimple (extMsgContent mc Nothing) + let evt = XMsgNew $ mcSimple mc (msg, _) <- sendDirectContactMessage user ct' evt ci <- saveSndChatItem user (CDDirectSnd ct') msg (CISndMsgContent mc) toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci] @@ -2067,12 +2117,12 @@ processChatCommand vr nm = \case toView $ CEvtNewChatItems user [ci] pure $ CRStartedConnectionToContact user ct' customUserProfile CVRConnectedContact ct' -> pure $ CRContactAlreadyExists user ct' - APIConnectPreparedGroup groupId incognito msgContent_ -> withUser $ \user -> do + APIConnectPreparedGroup {groupId, incognito, ownerContact, msgContent_} -> withUser $ \user -> do gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId case gInfo of GroupInfo {preparedGroup = Nothing} -> throwCmdError "group doesn't have link to connect" GroupInfo {useRelays = BoolDef True, preparedGroup = Just PreparedGroup {connLinkToConnect}} -> do - sLnk <- case toShortLinkContact connLinkToConnect of + sLnk <- case connShortLink' connLinkToConnect of Just sl -> pure sl Nothing -> throwChatError $ CEException "failed to retrieve relays: no short link" (FixedLinkData {linkConnReq = mainCReq@(CRContactUri crData), linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners, relays})) <- getShortLinkConnReq nm user sLnk @@ -2093,10 +2143,14 @@ processChatCommand vr nm = \case gInfo' <- withFastStore $ \db -> do gInfo' <- updatePreparedRelayedGroup db vr user gInfo mainCReq cReqHash incognitoProfile rootKey memberPrivKey publicMemberCount_ -- Pre-emptively create owner members with trusted keys from link data - forM_ owners $ \OwnerAuth {ownerId, ownerKey} -> - void $ createLinkOwnerMember db vr user gInfo' (MemberId ownerId) ownerKey + forM_ owners $ \OwnerAuth {ownerId, ownerKey} -> do + let ctId_ = case ownerContact of + Just GroupOwnerContact {contactId, memberId} + | memberId == MemberId ownerId -> Just contactId + _ -> Nothing + void $ createLinkOwnerMember db vr user gInfo' ctId_ (MemberId ownerId) ownerKey pure gInfo' - rs <- mapConcurrently (connectToRelay gInfo') relays + rs <- mapConcurrently (connectToRelay user gInfo') relays let relayFailed = \case (_, _, Left _) -> True; _ -> False (failed, succeeded) = partition relayFailed rs if null succeeded @@ -2123,23 +2177,6 @@ processChatCommand vr nm = \case isTempErr = \case (_, _, Left ChatErrorAgent {agentError = e}) -> temporaryOrHostError e _ -> False - connectToRelay gInfo' relayLink = do - gVar <- asks random - -- Save relayLink to re-use relay member record on retry (check by relayLink) - relayMember <- withFastStore $ \db -> getCreateRelayForMember db vr gVar user gInfo' relayLink - r <- tryAllErrors $ do - (fd@FixedLinkData {rootKey = relayKey, linkEntityId}, cData) <- getShortLinkConnReq nm user relayLink - relayLinkData_ <- liftIO $ decodeLinkUserData cData - case (relayLinkData_, linkEntityId) of - (Just RelayShortLinkData {relayProfile = p}, Just entityId) -> - withFastStore $ \db -> updateRelayMemberData db user relayMember (MemberId entityId) (MemberKey relayKey) p - _ -> throwChatError $ CEException "relay link: no relay link data or entity id" - let cReq = linkConnReq fd - relayLinkToConnect = CCLink cReq (Just relayLink) - void $ connectViaContact user (Just $ PCEGroup gInfo' relayMember) incognito relayLinkToConnect Nothing Nothing - -- Re-read member to get updated activeConn and updated data (from updateRelayMemberData) - relayMember' <- withFastStore $ \db -> getGroupMember db vr user groupId (groupMemberId' relayMember) - pure (relayLink, relayMember', r) retryRelayConnectionAsync gInfo' relayLink relayMember@GroupMember {activeConn} = do forM_ activeConn $ \conn -> do deleteAgentConnectionAsync $ aConnId conn @@ -2188,7 +2225,7 @@ processChatCommand vr nm = \case Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do -- TODO [relays] member: /c api to support groups with relays -- TODO - possibly by going through APIPrepareGroup -> APIConnectPreparedGroup - (ccLink, plan) <- connectPlan user cLink `catchAllErrors` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing)); _ -> throwError e + (ccLink, plan) <- connectPlan user cLink False Nothing `catchAllErrors` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing Nothing)); _ -> throwError e connectWithPlan user incognito ccLink plan Connect _ Nothing -> throwChatError CEInvalidConnReq APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do @@ -2206,7 +2243,7 @@ processChatCommand vr nm = \case toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct') throwError e ConnectSimplex incognito -> withUser $ \user -> do - plan <- contactRequestPlan user adminContactReq Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing)) + plan <- contactRequestPlan user adminContactReq Nothing Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing Nothing)) connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) plan DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId Nothing) cdm ClearContact cName -> withContactName cName $ \chatId -> APIClearChat $ ChatRef CTDirect chatId Nothing @@ -2227,7 +2264,7 @@ processChatCommand vr nm = \case userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData} (connId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink - let ccLink'' = if isTrue userChatRelay then createdRelayLink ccLink' else ccLink' + let ccLink'' = if isTrue userChatRelay then setShortLinkType CCTRelay ccLink' else ccLink' withFastStore $ \db -> createUserContactLink db user connId ccLink'' subMode pure $ CRUserContactLinkCreated user ccLink'' CreateMyAddress -> withUser $ \User {userId} -> @@ -2299,6 +2336,19 @@ processChatCommand vr nm = \case toChatRef <- getChatRef user toChatName asGroup <- getSendAsGroup user toChatRef processChatCommand vr nm $ APIForwardChatItems toChatRef asGroup (ChatRef CTLocal folderId Nothing) (forwardedItemId :| []) Nothing + SharePublicGroup shareGroupName toChatName -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user shareGroupName + toChatRef <- getChatRef user toChatName + sendRef <- case toChatRef of + ChatRef CTDirect ctId _ -> pure $ SRDirect ctId + ChatRef CTGroup gId scope_ -> do + gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId + pure $ SRGroup gId scope_ (useRelays' gInfo) + _ -> throwCmdError "unsupported share target" + processChatCommand vr nm (APIShareChatMsgContent (ChatRef CTGroup groupId Nothing) sendRef) >>= \case + CRChatMsgContent _ mc -> + processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] + r -> pure r SendMessage sendName msg -> withUser $ \user -> do let mc = MCText msg case sendName of @@ -2325,7 +2375,7 @@ processChatCommand vr nm = \case forM scope_ $ \(GSNMemberSupport mName_) -> GCSMemberSupport <$> mapM (getGroupMemberIdByName db user gId) mName_ (gInfo, cScope_,) <$> liftIO (getMessageMentions db user gId msg) - let sendRef = SRGroup (groupId' gInfo) cScope_ (sendAsGroup' gInfo) + let sendRef = SRGroup (groupId' gInfo) cScope_ (sendAsGroup' gInfo cScope_) processChatCommand vr nm $ APISendMessages sendRef False Nothing [ComposedMessage Nothing Nothing mc mentions] SNLocal -> do folderId <- withFastStore (`getUserNoteFolderId` user) @@ -2385,7 +2435,7 @@ processChatCommand vr nm = \case Right conn | directOrUsed ct -> (ct, conn) : ctConns _ -> ctConns ctSndEvent :: (Contact, Connection) -> (ConnOrGroupId, Maybe MsgSigning, ChatMsgEvent 'Json) - ctSndEvent (_, Connection {connId}) = (ConnectionId connId, Nothing, XMsgNew $ MCSimple (extMsgContent mc Nothing)) + ctSndEvent (_, Connection {connId}) = (ConnectionId connId, Nothing, XMsgNew $ mcSimple mc) ctMsgReq :: (Contact, Connection) -> SndMessage -> ChatMsgReq ctMsgReq (_, conn) SndMessage {msgId, msgBody} = (conn, MsgFlags {notification = hasNotification XMsgNew_}, (vrValue msgBody, [msgId])) combineResults :: (Contact, Connection) -> Either ChatError SndMessage -> Either ChatError ([Int64], PQEncryption) -> Either ChatError (Contact, SndMessage) @@ -2432,13 +2482,29 @@ processChatCommand vr nm = \case APINewPublicGroup userId incognito relayIds groupProfile -> withUserId userId $ \user -> do (gProfile', memberId, groupKeys, setupLink) <- prepareGroupLink user gInfo <- newGroup user incognito gProfile' True memberId (Just groupKeys) (Just 1) - (gLink, groupRelays) <- setupLink gInfo `catchAllErrors` \e -> do + (gLink, results) <- setupLink gInfo `catchAllErrors` \e -> do deleteInProgressGroup user gInfo throwError e - createNewGroupItems user gInfo - pure $ CRPublicGroupCreated user gInfo gLink groupRelays + case partitionEithers (map snd results) of + ([], groupRelays) -> do + createNewGroupItems user gInfo + pure $ CRPublicGroupCreated user gInfo gLink groupRelays + (errors@(e : _), _) -> do + deleteInProgressGroup user gInfo + -- If all errors are temporary (network, timeout, host), throw to allow retry + if all isTempErr errors + then throwError e + else do + let relayResults = map toRelayResult results + toRelayResult (r, Left e) = AddRelayResult r (Just e) + toRelayResult (r, Right _) = AddRelayResult r Nothing + pure $ CRPublicGroupCreationFailed user relayResults where - prepareGroupLink :: User -> CM (GroupProfile, MemberId, GroupKeys, GroupInfo -> CM (GroupLink, [GroupRelay])) + isTempErr :: ChatError -> Bool + isTempErr = \case + ChatErrorAgent {agentError = e} -> temporaryOrHostError e + _ -> False + prepareGroupLink :: User -> CM (GroupProfile, MemberId, GroupKeys, GroupInfo -> CM (GroupLink, [(UserChatRelay, Either ChatError GroupRelay)])) prepareGroupLink user = do gVar <- asks random groupLinkId <- GroupLinkId <$> drgRandomBytes 16 @@ -2449,8 +2515,8 @@ processChatCommand vr nm = \case crClientData = encodeJSON $ CRDataGroup groupLinkId -- prepare link with entityId as linkEntityId (no server request) (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey entityId True (Just crClientData) - ccLink' <- createdChannelLink <$> shortenCreatedLink ccLink - sLnk <- case toShortLinkContact ccLink' of + ccLink' <- setShortLinkType CCTChannel <$> shortenCreatedLink ccLink + sLnk <- case connShortLink' ccLink' of Just sl -> pure sl Nothing -> throwChatError $ CEException "failed to create relayed group link: no short link" -- generate owner key, OwnerAuth signed by root key @@ -2467,8 +2533,8 @@ processChatCommand vr nm = \case subRole <- asks $ channelSubscriberRole . config gLink <- withFastStore $ \db -> createGroupLink db gVar user gInfo connId ccLink' groupLinkId subRole subMode relays <- withFastStore $ \db -> mapM (getChatRelayById db user) (L.toList relayIds) - groupRelays <- addRelays user gInfo sLnk relays - pure (gLink, groupRelays) + results <- addRelays user gInfo sLnk relays + pure (gLink, results) pure (groupProfile', memberId, groupKeys, setupLink) NewPublicGroup incognito relayIds gProfile -> withUser $ \User {userId} -> processChatCommand vr nm $ APINewPublicGroup userId incognito relayIds gProfile @@ -2478,6 +2544,37 @@ processChatCommand vr nm = \case relays <- liftIO $ getGroupRelays db gInfo pure (gInfo, relays) pure $ CRGroupRelays user gInfo relays + APIAddGroupRelays groupId relayIds -> withUser $ \user -> withGroupLock "addGroupRelays" groupId $ do + (gInfo, existingRelays) <- withFastStore $ \db -> do + gi <- getGroupInfo db vr user groupId + rs <- liftIO $ getGroupRelays db gi + pure (gi, rs) + assertUserGroupRole gInfo GROwner + unless (useRelays' gInfo) $ throwCmdError "group does not use relays" + let existingRelayIds = map (\GroupRelay {userChatRelay = UserChatRelay {chatRelayId = DBEntityId rId}} -> rId) existingRelays + when (any (`elem` existingRelayIds) relayIds) $ throwCmdError "some relays are already in the group" + gLink@GroupLink {connLinkContact = ccLink} <- withFastStore $ \db -> getGroupLink db user gInfo + sLnk <- case connShortLink' ccLink of + Just sl -> pure sl + Nothing -> throwChatError $ CEException "group link has no short link" + relays <- withFastStore $ \db -> mapM (getChatRelayById db user) (L.toList relayIds) + results <- addRelays user gInfo sLnk relays + case partitionEithers (map snd results) of + ([], _) -> do + relays' <- withFastStore $ \db -> liftIO $ getGroupRelays db gInfo + pure $ CRGroupRelaysAdded user gInfo gLink relays' + (errors@(e : _), _) -> do + if all isTempErr errors + then throwError e + else do + let toRelayResult (r, Left e') = AddRelayResult r (Just e') + toRelayResult (r, Right _) = AddRelayResult r Nothing + pure $ CRGroupRelaysAddFailed user (map toRelayResult results) + where + isTempErr :: ChatError -> Bool + isTempErr = \case + ChatErrorAgent {agentError = e} -> temporaryOrHostError e + _ -> False APIAddMember groupId contactId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do -- TODO for large groups: no need to load all members to determine if contact is a member (group, contact) <- withFastStore $ \db -> (,) <$> getGroup db vr user groupId <*> getContact db vr user contactId @@ -2580,7 +2677,7 @@ processChatCommand vr nm = \case void $ sendGroupMessage user gInfo scope ([m] <> rcpModMs') msg when (maxVersion (memberChatVRange m) < groupKnockingVersion) $ forM_ (memberConn m) $ \mConn -> do - let msg2 = XMsgNew $ MCSimple $ extMsgContent (MCText acceptedToGroupMessage) Nothing + let msg2 = XMsgNew $ mcSimple (MCText acceptedToGroupMessage) void $ sendDirectMemberMessage mConn msg2 groupId when (memberCategory m == GCInviteeMember) $ do introduceToRemaining vr user gInfo m {memberRole = role} @@ -2833,18 +2930,35 @@ processChatCommand vr nm = \case filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo withGroupLock "leaveGroup" groupId $ do cancelFilesInProgress user filesInfo - (members, recipients) <- getRecipients user gInfo - msg <- sendGroupMessage' user gInfo recipients XGrpLeave + msg <- + if useRelays' gInfo && isRelay membership + then leaveChannelRelay gInfo + else leaveGroupSendMsg user gInfo (gInfo', scopeInfo) <- mkLocalGroupChatScope gInfo ci <- saveSndChatItem user (CDGroupSnd gInfo' scopeInfo) msg (CISndGroupEvent SGEUserLeft) toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo' scopeInfo) ci] -- TODO delete direct connections that were unused deleteGroupLinkIfExists user gInfo' -- member records are not deleted to keep history - deleteMembersConnections' user members True withFastStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft pure $ CRLeftMemberUser user gInfo' {membership = membership {memberStatus = GSMemLeft}} where + -- Relay leaving channel: create delivery job for cursor-based sending and async connection cleanup. + leaveChannelRelay gInfo = do + msg@SndMessage {msgBody, signedMsg_} <- + liftEither . runIdentity =<< lift (createSndMessages $ Identity (GroupId groupId, groupMsgSigning gInfo XGrpLeave, XGrpLeave)) + let body = encodeBatchElement signedMsg_ msgBody + withFastStore' $ \db -> do + deleteGroupDeliveryTasks db gInfo + deleteGroupDeliveryJobs db gInfo + createMsgDeliveryJob db gInfo (DJSGroup {jobSpec = DJRelayRemoved}) Nothing body + lift . void $ getDeliveryJobWorker True (groupId, DWSGroup) + pure msg + leaveGroupSendMsg user gInfo = do + (members, recipients) <- getRecipients user gInfo + msg <- sendGroupMessage' user gInfo recipients XGrpLeave + deleteMembersConnections' user members True + pure msg getRecipients user gInfo | useRelays' gInfo = do relays <- withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo @@ -2919,7 +3033,7 @@ processChatCommand vr nm = \case userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData} crClientData = encodeJSON $ CRDataGroup groupLinkId (connId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) (Just crClientData) IKPQOff subMode - ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink + ccLink' <- setShortLinkType CCTGroup <$> shortenCreatedLink ccLink gVar <- asks random gLink <- withFastStore $ \db -> createGroupLink db gVar user gInfo connId ccLink' groupLinkId mRole subMode pure $ CRGroupLinkCreated user gInfo gLink @@ -3042,7 +3156,7 @@ processChatCommand vr nm = \case qiId <- getGroupChatItemIdByText db user gId cName quotedMsg (gInfo, qiId,) <$> liftIO (getMessageMentions db user gId msg) let mc = MCText msg - processChatCommand vr nm $ APISendMessages (SRGroup (groupId' gInfo) Nothing (sendAsGroup' gInfo)) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc mentions] + processChatCommand vr nm $ APISendMessages (SRGroup (groupId' gInfo) Nothing (sendAsGroup' gInfo Nothing)) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc mentions] ClearNoteFolder -> withUser $ \user -> do folderId <- withFastStore (`getUserNoteFolderId` user) processChatCommand vr nm $ APIClearChat (ChatRef CTLocal folderId Nothing) @@ -3318,7 +3432,7 @@ processChatCommand vr nm = \case _ -> throwCmdError "not supported" pure $ ChatRef cType chatId Nothing getSendAsGroup :: User -> ChatRef -> CM ShowGroupAsSender - getSendAsGroup user' (ChatRef CTGroup chatId _) = sendAsGroup' <$> withFastStore (\db -> getGroupInfo db vr user' chatId) + getSendAsGroup user' (ChatRef CTGroup chatId scope) = (`sendAsGroup'` scope) <$> withFastStore (\db -> getGroupInfo db vr user' chatId) getSendAsGroup _ _ = pure False getChatRefAndMentions :: User -> ChatName -> Text -> CM (ChatRef, Map MemberName GroupMemberId) getChatRefAndMentions user cName msg = do @@ -3488,6 +3602,44 @@ processChatCommand vr nm = \case ct' <- withStore $ \db -> getContact db vr user contactId pure $ CRSentInvitationToContact user ct' incognitoProfile _ -> throwCmdError "contact already has connection" + connectToRelay :: User -> GroupInfo -> ShortLinkContact -> CM (ShortLinkContact, GroupMember, Either ChatError ()) + connectToRelay user gInfo relayLink = do + gVar <- asks random + -- Save relayLink to re-use relay member record on retry (check by relayLink) + relayMember <- withFastStore $ \db -> getCreateRelayForMember db vr gVar user gInfo relayLink + r <- tryAllErrors $ do + (fd@FixedLinkData {rootKey = relayKey, linkEntityId}, cData) <- getShortLinkConnReq nm user relayLink + relayLinkData_ <- liftIO $ decodeLinkUserData cData + case (relayLinkData_, linkEntityId) of + (Just RelayShortLinkData {relayProfile = p}, Just entityId) -> + withFastStore $ \db -> updateRelayMemberData db user relayMember (MemberId entityId) (MemberKey relayKey) p + _ -> throwChatError $ CEException "relay link: no relay link data or entity id" + let cReq = linkConnReq fd + relayLinkToConnect = CCLink cReq (Just relayLink) + void $ connectViaContact user (Just $ PCEGroup gInfo relayMember) (incognitoMembership gInfo) relayLinkToConnect Nothing Nothing + relayMember' <- withFastStore $ \db -> getGroupMember db vr user (groupId' gInfo) (groupMemberId' relayMember) + pure (relayLink, relayMember', r) + syncSubscriberRelays :: User -> GroupInfo -> [ShortLinkContact] -> CM () + syncSubscriberRelays user gInfo currentRelayLinks = void . tryAllErrors $ do + localRelayMembers <- withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo + let activeRelayMembers = filter memberCurrent localRelayMembers + memberRelayLink GroupMember {relayLink = rl} = rl + localRelayLinks = mapMaybe memberRelayLink activeRelayMembers + newRelayLinks = filter (`notElem` localRelayLinks) currentRelayLinks + forM_ newRelayLinks $ \rlnk -> void . tryAllErrors $ + connectToRelay user gInfo rlnk + forM_ localRelayMembers $ \m -> + case memberRelayLink m of + -- Remove relay if its link is no longer in the current link data. + -- Inactive relays (e.g. left) are only cleaned up when no active relays remain, + -- as that is the only case where the owner's relay removal can't be forwarded. + Just rlnk | rlnk `notElem` currentRelayLinks, + memberCurrent m || null activeRelayMembers -> + void . tryAllErrors $ do + deleteMemberConnection m + deleteOrUpdateMemberRecord user gInfo m + _ -> pure () + prepareContact :: User -> ConnReqContact -> PQSupport -> CM (ConnId, VersionChat) prepareContact user cReq pqSup = do -- 0) toggle disabled - PQSupportOff @@ -3772,7 +3924,7 @@ processChatCommand vr nm = \case createNewGroupItems user gInfo = do let cd = CDGroupSnd gInfo Nothing createInternalChatItem user cd CIChatBanner (Just epochStart) - createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing + createInternalChatItem user cd (CISndGroupE2EEInfo $ e2eInfoGroup gInfo) Nothing createGroupFeatureItems user cd CISndGroupFeature gInfo sendGrpInvitation :: User -> Contact -> GroupInfo -> GroupMember -> ConnReqInvitation -> CM () sendGrpInvitation user ct@Contact {contactId, localDisplayName} gInfo@GroupInfo {groupId, groupProfile, membership, businessChat} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do @@ -3795,15 +3947,12 @@ processChatCommand vr nm = \case toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] forM_ (timed_ >>= timedDeleteAt') $ startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId' ci) - addRelays :: User -> GroupInfo -> ShortLinkContact -> [UserChatRelay] -> CM [GroupRelay] + addRelays :: User -> GroupInfo -> ShortLinkContact -> [UserChatRelay] -> CM [(UserChatRelay, Either ChatError GroupRelay)] addRelays user gInfo@GroupInfo {membership} groupSLink relays = mapConcurrently addRelay relays where - addRelay :: UserChatRelay -> CM GroupRelay - addRelay relay@UserChatRelay {address} = do - -- TODO [relays] owner: track and reuse relay profiles - -- TODO - single profile linked to relay configuration record (chat_relays) - -- TODO - update when fetching link data from relay address + addRelay :: UserChatRelay -> CM (UserChatRelay, Either ChatError GroupRelay) + addRelay relay@UserChatRelay {address} = fmap (relay,) . tryAllErrors $ do (FixedLinkData {linkConnReq = cReq}, _cData) <- getShortLinkConnReq nm user address lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOff cReq) >>= \case Nothing -> throwChatError CEInvalidConnReq @@ -3896,28 +4045,29 @@ processChatCommand vr nm = \case pure (gId, chatSettings) _ -> throwCmdError "not supported" processChatCommand vr nm $ APISetChatSettings (ChatRef cType chatId Nothing) $ updateSettings chatSettings - connectPlan :: User -> AConnectionLink -> CM (ACreatedConnLink, ConnectionPlan) - connectPlan user (ACL SCMInvitation cLink) = case cLink of - CLFull cReq -> invitationReqAndPlan cReq Nothing Nothing + connectPlan :: User -> AConnectionLink -> Bool -> Maybe LinkOwnerSig -> CM (ACreatedConnLink, ConnectionPlan) + connectPlan user (ACL SCMInvitation cLink) _ sig_ = case cLink of + CLFull cReq -> invitationReqAndPlan cReq Nothing Nothing Nothing CLShort l -> do let l' = serverShortLink l knownLinkPlans l' >>= \case Just r -> pure r Nothing -> do - (FixedLinkData {linkConnReq = cReq}, cData) <- getShortLinkConnReq nm user l' + (FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l' contactSLinkData_ <- liftIO $ decodeLinkUserData cData - invitationReqAndPlan cReq (Just l') contactSLinkData_ + let ov = verifyLinkOwner rootKey [] l sig_ + invitationReqAndPlan cReq (Just l') contactSLinkData_ ov where knownLinkPlans l' = withFastStore $ \db -> do let inv cReq = ACCL SCMInvitation $ CCLink cReq (Just l') liftIO (getConnectionEntityViaShortLink db vr user l') >>= \case - Just (cReq, ent) -> pure $ Just (inv cReq, invitationEntityPlan Nothing ent) + Just (cReq, ent) -> pure $ Just (inv cReq, invitationEntityPlan Nothing Nothing ent) -- deleted contact is returned as known, as invitation link cannot be re-used too connect anyway Nothing -> bimap inv (CPInvitationLink . ILPKnown) <$$> getContactViaShortLinkToConnect db vr user l' - invitationReqAndPlan cReq sLnk_ contactSLinkData_ = do - plan <- invitationRequestPlan user cReq contactSLinkData_ `catchAllErrors` (pure . CPError) + invitationReqAndPlan cReq sLnk_ cld ov = do + plan <- invitationRequestPlan user cReq cld ov `catchAllErrors` (pure . CPError) pure (ACCL SCMInvitation (CCLink cReq sLnk_), plan) - connectPlan user (ACL SCMContact cLink) = case cLink of + connectPlan user (ACL SCMContact cLink) resolveKnown sig_ = case cLink of CLFull cReq -> do plan <- contactOrGroupRequestPlan user cReq `catchAllErrors` (pure . CPError) pure (ACCL SCMContact $ CCLink cReq Nothing, plan) @@ -3927,12 +4077,14 @@ processChatCommand vr nm = \case knownLinkPlans >>= \case Just r -> pure r Nothing -> do - (FixedLinkData {linkConnReq = cReq}, cData) <- getShortLinkConnReq nm user l' + (FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l' withFastStore' (\db -> getContactWithoutConnViaShortAddress db vr user l') >>= \case Just ct' | not (contactDeleted ct') -> pure (con cReq, CPContactAddress (CAPContactViaAddress ct')) _ -> do contactSLinkData_ <- liftIO $ decodeLinkUserData cData - plan <- contactRequestPlan user cReq contactSLinkData_ + let ContactLinkData _ UserContactData {owners} = cData + ov = verifyLinkOwner rootKey owners l' sig_ + plan <- contactRequestPlan user cReq contactSLinkData_ ov pure (con cReq, plan) where knownLinkPlans = withFastStore $ \db -> @@ -3948,30 +4100,43 @@ processChatCommand vr nm = \case where l' = serverShortLink l con cReq = ACCL SCMContact $ CCLink cReq (Just l') - gPlan (cReq, g) = if memberRemoved (membership g) then Nothing else Just (con cReq, CPGroupLink (GLPKnown g)) + gPlan (cReq, g) = if memberRemoved (membership g) then Nothing else Just (con cReq, CPGroupLink (GLPKnown g (BoolDef False) Nothing (ListDef []))) groupShortLinkPlan = knownLinkPlans >>= \case + Just (_, CPGroupLink (GLPKnown g _ _ _)) + | resolveKnown -> resolveKnownGroup g Just r -> pure r Nothing -> do - (fd, cData@(ContactLinkData _ UserContactData {direct, relays})) <- getShortLinkConnReq nm user l' - let FixedLinkData {linkConnReq = cReq, linkEntityId} = fd - linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, publicGroupId = B64UrlByteString <$> linkEntityId} + (fd, cData@(ContactLinkData _ UserContactData {direct, owners, relays})) <- getShortLinkConnReq' nm user l' groupSLinkData_ <- liftIO $ decodeLinkUserData cData - -- Cross-validate linkEntityId and publicGroupId from profile: - -- for channels both must be present and match, for p2p groups both must be absent - let profilePGId = groupSLinkData_ >>= \GroupShortLinkData {groupProfile = GroupProfile {publicGroup}} -> - fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId) publicGroup - case (B64UrlByteString <$> linkEntityId, profilePGId) of - (Just entityId, Just publicGroupId) | entityId == publicGroupId -> pure () - (Nothing, Nothing) -> pure () - _ -> throwChatError CEInvalidConnReq - plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ - pure (con cReq, plan) + if not direct && null relays + then pure (con (linkConnReq fd), CPGroupLink (GLPNoRelays groupSLinkData_)) + else do + let FixedLinkData {linkConnReq = cReq, linkEntityId, rootKey} = fd + linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, publicGroupId = B64UrlByteString <$> linkEntityId} + let profilePGId = groupSLinkData_ >>= \GroupShortLinkData {groupProfile = GroupProfile {publicGroup}} -> + fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId) publicGroup + case (B64UrlByteString <$> linkEntityId, profilePGId) of + (Just entityId, Just publicGroupId) | entityId == publicGroupId -> pure () + (Nothing, Nothing) -> pure () + _ -> throwChatError CEInvalidConnReq + let ov = verifyLinkOwner rootKey owners l' sig_ + plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov + pure (con cReq, plan) where knownLinkPlans = withFastStore $ \db -> liftIO (getGroupInfoViaUserShortLink db vr user l') >>= \case Just (cReq, g) -> pure $ Just (con cReq, CPGroupLink (GLPOwnLink g)) Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db vr user l' + resolveKnownGroup g = do + (fd@FixedLinkData {rootKey = rk}, cData@(ContactLinkData _ UserContactData {owners})) <- getShortLinkConnReq' nm user l' + groupSLinkData_ <- liftIO $ decodeLinkUserData cData + let ov = verifyLinkOwner rk owners l' sig_ + glOwners = map (\OwnerAuth {ownerId, ownerKey} -> GroupLinkOwner {memberId = MemberId ownerId, memberKey = ownerKey}) owners + (g', updated) <- case groupSLinkData_ of + Just sLinkData -> updateGroupFromLinkData user g sLinkData + _ -> pure (g, False) + pure (con (linkConnReq fd), CPGroupLink (GLPKnown g' (BoolDef updated) ov (ListDef glOwners))) connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse connectWithPlan user@User {userId} incognito ccLink plan | connectionPlanProceed plan = do @@ -3981,9 +4146,9 @@ processChatCommand vr nm = \case processChatCommand vr nm $ APIConnectContactViaAddress userId incognito contactId _ -> processChatCommand vr nm $ APIConnect userId incognito $ Just ccLink | otherwise = pure $ CRConnectionPlan user ccLink plan - invitationRequestPlan :: User -> ConnReqInvitation -> Maybe ContactShortLinkData -> CM ConnectionPlan - invitationRequestPlan user cReq contactSLinkData_ = do - maybe (CPInvitationLink (ILPOk contactSLinkData_)) (invitationEntityPlan contactSLinkData_) + invitationRequestPlan :: User -> ConnReqInvitation -> Maybe ContactShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan + invitationRequestPlan user cReq cld ov = do + maybe (CPInvitationLink (ILPOk cld ov)) (invitationEntityPlan cld ov) <$> withFastStore' (\db -> getConnectionEntityByConnReq db vr user $ invCReqSchemas cReq) where invCReqSchemas :: ConnReqInvitation -> (ConnReqInvitation, ConnReqInvitation) @@ -3991,15 +4156,15 @@ processChatCommand vr nm = \case ( CRInvitationUri crData {crScheme = SSSimplex} e2e, CRInvitationUri crData {crScheme = simplexChat} e2e ) - invitationEntityPlan :: Maybe ContactShortLinkData -> ConnectionEntity -> ConnectionPlan - invitationEntityPlan contactSLinkData_ = \case + invitationEntityPlan :: Maybe ContactShortLinkData -> Maybe OwnerVerification -> ConnectionEntity -> ConnectionPlan + invitationEntityPlan cld ov = \case RcvDirectMsgConnection Connection {connStatus, contactConnInitiated} ct_ -> case ct_ of Just ct | contactActive ct -> CPInvitationLink (ILPKnown ct) - | otherwise -> CPInvitationLink (ILPOk contactSLinkData_) + | otherwise -> CPInvitationLink (ILPOk cld ov) Nothing | connStatus == ConnNew && contactConnInitiated -> CPInvitationLink ILPOwnLink - | connStatus == ConnPrepared -> CPInvitationLink (ILPOk contactSLinkData_) + | connStatus == ConnPrepared -> CPInvitationLink (ILPOk cld ov) | otherwise -> CPInvitationLink (ILPConnecting Nothing) _ -> CPError $ ChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" contactOrGroupRequestPlan :: User -> ConnReqContact -> CM ConnectionPlan @@ -4007,10 +4172,10 @@ processChatCommand vr nm = \case let ConnReqUriData {crClientData} = crData groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli case groupLinkId of - Nothing -> contactRequestPlan user cReq Nothing - Just _ -> groupJoinRequestPlan user cReq Nothing Nothing - contactRequestPlan :: User -> ConnReqContact -> Maybe ContactShortLinkData -> CM ConnectionPlan - contactRequestPlan user (CRContactUri crData) contactSLinkData_ = do + Nothing -> contactRequestPlan user cReq Nothing Nothing + Just _ -> groupJoinRequestPlan user cReq Nothing Nothing Nothing + contactRequestPlan :: User -> ConnReqContact -> Maybe ContactShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan + contactRequestPlan user (CRContactUri crData) cld ov = do let cReqSchemas = contactCReqSchemas crData cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas withFastStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case @@ -4020,19 +4185,19 @@ processChatCommand vr nm = \case Nothing -> withFastStore' (\db -> getContactWithoutConnViaAddress db vr user cReqSchemas) >>= \case Just ct | not (contactDeleted ct) -> pure $ CPContactAddress (CAPContactViaAddress ct) - _ -> pure $ CPContactAddress (CAPOk contactSLinkData_) + _ -> pure $ CPContactAddress (CAPOk cld ov) Just (RcvDirectMsgConnection Connection {connStatus} Nothing) - | connStatus == ConnPrepared -> pure $ CPContactAddress (CAPOk contactSLinkData_) + | connStatus == ConnPrepared -> pure $ CPContactAddress (CAPOk cld ov) | otherwise -> pure $ CPContactAddress CAPConnectingConfirmReconnect Just (RcvDirectMsgConnection _ (Just ct)) | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct) - | contactDeleted ct -> pure $ CPContactAddress (CAPOk contactSLinkData_) + | contactDeleted ct -> pure $ CPContactAddress (CAPOk cld ov) | otherwise -> pure $ CPContactAddress (CAPKnown ct) -- TODO [short links] RcvGroupMsgConnection branch is deprecated? (old group link protocol?) - Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing Nothing + Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing Nothing Nothing Just _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection" - groupJoinRequestPlan :: User -> ConnReqContact -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> CM ConnectionPlan - groupJoinRequestPlan user (CRContactUri crData) groupSLinkInfo_ groupSLinkData_ = do + groupJoinRequestPlan :: User -> ConnReqContact -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan + groupJoinRequestPlan user (CRContactUri crData) linkInfo gld ov = do let cReqSchemas = contactCReqSchemas crData cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db vr user cReqSchemas) >>= \case @@ -4041,21 +4206,21 @@ processChatCommand vr nm = \case connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db vr user cReqHashes gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db vr user cReqHashes case (gInfo_, connEnt_) of - (Nothing, Nothing) -> pure $ CPGroupLink (GLPOk groupSLinkInfo_ groupSLinkData_) + (Nothing, Nothing) -> pure $ CPGroupLink (GLPOk linkInfo gld ov) -- TODO [short links] RcvDirectMsgConnection branches are deprecated? (old group link protocol?) (Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect (Nothing, Just (RcvDirectMsgConnection _ (Just ct))) | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_) - | otherwise -> pure $ CPGroupLink (GLPOk groupSLinkInfo_ groupSLinkData_) + | otherwise -> pure $ CPGroupLink (GLPOk linkInfo gld ov) (Nothing, Just _) -> throwCmdError "found connection entity is not RcvDirectMsgConnection" - (Just gInfo, _) -> groupPlan gInfo groupSLinkInfo_ groupSLinkData_ - groupPlan :: GroupInfo -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> CM ConnectionPlan - groupPlan gInfo@GroupInfo {membership} groupSLinkInfo_ groupSLinkData_ - | memberStatus membership == GSMemRejected = pure $ CPGroupLink (GLPKnown gInfo) + (Just gInfo, _) -> groupPlan gInfo linkInfo gld ov + groupPlan :: GroupInfo -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan + groupPlan gInfo@GroupInfo {membership} linkInfo gld ov + | memberStatus membership == GSMemRejected = pure $ CPGroupLink (GLPKnown gInfo (BoolDef False) ov (ListDef [])) | not (memberActive membership) && not (memberRemoved membership) = pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo) - | memberActive membership = pure $ CPGroupLink (GLPKnown gInfo) - | otherwise = pure $ CPGroupLink (GLPOk groupSLinkInfo_ groupSLinkData_) + | memberActive membership = pure $ CPGroupLink (GLPKnown gInfo (BoolDef False) ov (ListDef [])) + | otherwise = pure $ CPGroupLink (GLPOk linkInfo gld ov) contactCReqSchemas :: ConnReqUriData -> (ConnReqContact, ConnReqContact) contactCReqSchemas crData = ( CRContactUri crData {crScheme = SSSimplex}, @@ -4067,6 +4232,16 @@ processChatCommand vr nm = \case serverShortLink = \case CSLInvitation _ srv lnkId linkKey -> CSLInvitation SLSServer srv lnkId linkKey CSLContact _ ct srv linkKey -> CSLContact SLSServer ct srv linkKey + verifyLinkOwner :: ConnectionModeI m => C.PublicKeyEd25519 -> [OwnerAuth] -> ConnShortLink m -> Maybe LinkOwnerSig -> Maybe OwnerVerification + verifyLinkOwner rootKey owners connLink = + fmap $ \LinkOwnerSig {ownerId, chatBinding = B64UrlByteString bindingBytes, ownerSig} -> + let signedData = bindingBytes <> smpEncode connLink + findOwner (B64UrlByteString oId) = find (\OwnerAuth {ownerId = oId'} -> oId' == oId) owners + in case maybe (Just rootKey) (fmap ownerKey . findOwner) ownerId of + Nothing -> OVFailed "unknown owner" + Just key + | C.verify' key ownerSig signedData -> OVVerified + | otherwise -> OVFailed "signature verification failed" contactShortLinkData :: Profile -> Maybe AddressSettings -> UserLinkData contactShortLinkData p settings = let msg = autoReply =<< settings @@ -4078,7 +4253,7 @@ processChatCommand vr nm = \case encodeShortLinkData $ RelayAddressLinkData {relayProfile = RelayProfile {displayName, fullName, shortDescr, image}} updatePCCShortLinkData :: PendingContactConnection -> Profile -> CM (Maybe ShortLinkInvitation) updatePCCShortLinkData conn@PendingContactConnection {connLinkInv} profile = - forM (connShortLink =<< connLinkInv) $ \_ -> do + forM (connShortLink' =<< connLinkInv) $ \_ -> do let userData = contactShortLinkData profile Nothing userLinkData = UserInvLinkData userData shortenShortLink' =<< withAgent (\a -> setConnShortLink a nm (aConnId' conn) SCMInvitation userLinkData Nothing) @@ -4142,9 +4317,9 @@ processChatCommand vr nm = \case prepareMsgs :: NonEmpty (ComposedMessageReq, Maybe FileInvitation) -> Maybe CITimed -> CM (NonEmpty (MsgContainer, Maybe (CIQuote 'CTDirect))) prepareMsgs cmsFileInvs timed_ = withFastStore $ \db -> forM cmsFileInvs $ \((ComposedMessage {quotedItemId, msgContent = mc}, itemForwarded, _, _), fInv_) -> do - case (quotedItemId, itemForwarded) of - (Nothing, Nothing) -> pure (MCSimple (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live) Nothing Nothing), Nothing) - (Nothing, Just _) -> pure (MCForward (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live) Nothing Nothing), Nothing) + (mc', quotedItem_) <- case (quotedItemId, itemForwarded) of + (Nothing, Nothing) -> pure (mcSimple mc, Nothing) + (Nothing, Just _) -> pure (mcForward mc, Nothing) (Just qiId, Nothing) -> do CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- getDirectChatItem db user contactId qiId @@ -4152,8 +4327,9 @@ processChatCommand vr nm = \case let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing} qmc = quoteContent mc origQmc file quotedItem = CIQuote {chatDir = qd, itemId = Just qiId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText} - pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live) Nothing Nothing), Just quotedItem) + pure (mcQuote QuotedMsg {msgRef, content = qmc} mc, Just quotedItem) (Just _, Just _) -> throwError SEInvalidQuote + pure (mc' {file = fInv_, ttl = ttl' <$> timed_, live = justTrue live}, quotedItem_) where quoteData :: ChatItem c d -> ExceptT StoreError IO (MsgContent, CIQDirection 'CTDirect, Bool) quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwError SEInvalidQuote @@ -4378,7 +4554,7 @@ processChatCommand vr nm = \case ChatRef CTDirect cId _ -> a $ SRDirect cId ChatRef CTGroup gId scope -> do gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId - a $ SRGroup gId scope (sendAsGroup' gInfo) + a $ SRGroup gId scope (sendAsGroup' gInfo scope) _ -> throwCmdError "not supported" getSharedMsgId :: CM SharedMsgId getSharedMsgId = do @@ -4612,12 +4788,41 @@ deleteInProgressGroup user gInfo = do withFastStore' $ \db -> deleteGroup db user gInfo runRelayGroupLinkChecks :: User -> CM () -runRelayGroupLinkChecks _user = do - -- TODO [relays] relay: periodically check presence of relay link in group links of served groups - -- TODO - retrieve group link data - -- TODO - if relay link is present, update relay status to RSActive - -- TODO - if relay link is absent and status was RSActive -> update to new "Removed" status? - pure () +runRelayGroupLinkChecks user = do + interval <- asks (relayChecksInterval . config) + liftIO $ threadDelay' $ diffToMicroseconds interval + forever $ do + flip catchAllErrors eToView $ do + lift waitChatStartedAndActivated + checkRelayServedGroups + checkRelayInactiveGroups + liftIO $ threadDelay' $ diffToMicroseconds interval + where + checkRelayServedGroups = do + vr <- chatVersionRange + relayGroups <- withStore' $ \db -> getRelayServedGroups db vr user + forM_ relayGroups $ \gInfo@GroupInfo {groupProfile = gp} -> flip catchAllErrors eToView $ do + case publicGroup gp of + Just PublicGroupProfile {groupLink = sLnk} -> do + (_, ContactLinkData _ UserContactData {relays = relayLinks}) <- + getShortLinkConnReq' NRMBackground user sLnk + gLink_ <- withStore' $ \db -> runExceptT $ getGroupLink db user gInfo + case gLink_ of + Right GroupLink {connLinkContact = CCLink _ (Just ourLink)} -> + if ourLink `elem` relayLinks + then do + -- TODO [relays] emit event to UI when relay own status promoted to RSActive + -- CEvtGroupRelayUpdated requires GroupRelay (owner-side), not available on relay side + void $ withStore' $ \db -> updateRelayOwnStatusFromTo db gInfo RSAccepted RSActive + else void $ withStore' $ \db -> updateRelayOwnStatusFromTo db gInfo RSActive RSInactive + _ -> pure () + _ -> pure () + checkRelayInactiveGroups = do + vr <- chatVersionRange + ttl <- asks (relayInactiveTTL . config) + inactiveGroups <- withStore' $ \db -> getRelayInactiveGroups db vr user ttl + forM_ inactiveGroups $ \gInfo -> flip catchAllErrors eToView $ + deleteGroupConnections user gInfo False expireChatItems :: User -> Int64 -> Bool -> CM () expireChatItems user@User {userId} globalTTL sync = do @@ -4780,6 +4985,7 @@ chatCommandP = "/_reaction members " *> (APIGetReactionMembers <$> A.decimal <* " #" <*> A.decimal <* A.space <*> A.decimal <* A.space <*> (knownReaction <$?> jsonP)), "/_forward plan " *> (APIPlanForwardChatItems <$> chatRefP <*> _strP), "/_forward " *> (APIForwardChatItems <$> chatRefP <*> (" as_group=" *> onOffP <|> pure False) <* A.space <*> chatRefP <*> _strP <*> sendMessageTTLP), + "/_share chat content " *> (APIShareChatMsgContent <$> chatRefP <* A.space <*> sendRefP), "/_read user " *> (APIUserRead <$> A.decimal), "/read user" $> UserRead, "/_read chat " *> (APIChatRead <$> chatRefP), @@ -4908,9 +5114,10 @@ chatCommandP = ("/help" <|> "/h") $> ChatHelp HSMain, ("/group" <|> "/g") *> (NewGroup <$> incognitoP <* A.space <* char_ '#' <*> groupProfile), "/_group " *> (APINewGroup <$> A.decimal <*> incognitoOnOffP <* A.space <*> jsonP), - ("/public group" <|> "/pg") *> (NewPublicGroup <$> incognitoP <* " relays=" <*> strP <* A.space <* char_ '#' <*> groupProfile), + ("/public group" <|> "/pg") *> (NewPublicGroup <$> incognitoP <* " relays=" <*> strP <* A.space <* char_ '#' <*> channelProfile), "/_public group " *> (APINewPublicGroup <$> A.decimal <*> incognitoOnOffP <*> _strP <* A.space <*> jsonP), "/_get relays #" *> (APIGetGroupRelays <$> A.decimal), + "/_add relays #" *> (APIAddGroupRelays <$> A.decimal <*> _strP), ("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)), ("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayNameP <*> (" mute" $> MFNone <|> pure MFAll)), "/accept member " *> char_ '#' *> (AcceptMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)), @@ -4951,13 +5158,13 @@ chatCommandP = (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayNameP <* A.space <* char_ '@' <*> (Just <$> displayNameP) <* A.space <*> quotedMsg <*> msgTextP), "/_contacts " *> (APIListContacts <$> A.decimal), "/contacts" $> ListContacts, - "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing)), + "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing) <*> ((" resolve=" *> onOffP) <|> pure False) <*> optional (" sig=" *> jsonP)), "/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <* A.space <*> jsonP), "/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP' <*> (" direct=" *> onOffP <|> pure True) <* A.space <*> jsonP), "/_set contact user @" *> (APIChangePreparedContactUser <$> A.decimal <* A.space <*> A.decimal), "/_set group user #" *> (APIChangePreparedGroupUser <$> A.decimal <* A.space <*> A.decimal), "/_connect contact @" *> (APIConnectPreparedContact <$> A.decimal <*> incognitoOnOffP <*> optional (A.space *> msgContentP)), - "/_connect group #" *> (APIConnectPreparedGroup <$> A.decimal <*> incognitoOnOffP <*> optional (A.space *> msgContentP)), + "/_connect group #" *> (APIConnectPreparedGroup <$> A.decimal <*> incognitoOnOffP <*> optional (A.space *> ownerContactP) <*> optional (A.space *> msgContentP)), "/_connect " *> (APIAddContact <$> A.decimal <*> incognitoOnOffP), "/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> connLinkP_), "/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP), @@ -4968,6 +5175,7 @@ chatCommandP = ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <* A.space <* A.char '@' <*> (Just <$> displayNameP) <* A.space <*> msgTextP, ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <*> pure Nothing <* A.space <*> msgTextP, ForwardLocalMessage <$> chatNameP <* " <- * " <*> msgTextP, + "/share chat #" *> (SharePublicGroup <$> displayNameP <* A.space <*> chatNameP), SendMessage <$> sendNameP <* A.space <*> msgTextP, "@#" *> (SendMemberContactMessage <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <* A.space <*> msgTextP), "/accept_member_contact @" *> (AcceptMemberContact <$> displayNameP), @@ -5037,6 +5245,7 @@ chatCommandP = "/set disappear @" *> (SetContactTimedMessages <$> displayNameP <*> optional (A.space *> timedMessagesEnabledP)), "/set disappear " *> (SetUserTimedMessages <$> (("yes" $> True) <|> ("no" $> False))), "/set reports #" *> (SetGroupFeature (AGFNR SGFReports) <$> displayNameP <*> _strP), + "/set support #" *> (SetGroupFeature (AGFNR SGFSupport) <$> displayNameP <*> (A.space *> strP)), "/set links #" *> (SetGroupFeatureRole (AGFR SGFSimplexLinks) <$> displayNameP <*> _strP <*> optional memberRole), "/set admission review #" *> (SetGroupMemberAdmissionReview <$> displayNameP <*> (A.space *> memberCriteriaP)), ("/incognito" <* optional (A.space *> onOffP)) $> ChatHelp HSIncognito, @@ -5086,6 +5295,7 @@ chatCommandP = ((Just <$> connLinkP) <|> A.takeTill (== ' ') $> Nothing) incognitoP = (A.space *> ("incognito" <|> "i")) $> True <|> pure False incognitoOnOffP = (A.space *> "incognito=" *> onOffP) <|> pure False + ownerContactP = "contact=" *> (GroupOwnerContact <$> A.decimal <* " owner=" <*> strP) imagePrefix = (<>) <$> "data:" <*> ("image/png;base64," <|> "image/jpg;base64,") imageP = safeDecodeUtf8 <$> ((<>) <$> imagePrefix <*> (B64.encode <$> base64P)) chatTypeP = A.char '@' $> CTDirect <|> A.char '#' $> CTGroup <|> A.char '*' $> CTLocal <|> A.char ':' $> CTContactConnection @@ -5175,6 +5385,10 @@ chatCommandP = history = Just HistoryGroupPreference {enable = FEOn} } pure GroupProfile {displayName = gName, fullName = "", shortDescr, description = Nothing, image = Nothing, publicGroup = Nothing, groupPreferences, memberAdmission = Nothing} + channelProfile = do + p@GroupProfile {groupPreferences = prefs_} <- groupProfile + let prefs = (fromMaybe emptyGroupPrefs prefs_) {support = Just SupportGroupPreference {enable = FEOff}} :: GroupPreferences + pure p {groupPreferences = Just prefs} memberCriteriaP = ("all" $> Just MCAll) <|> ("off" $> Nothing) shortDescrP = do descr <- A.takeWhile1 isSpace *> (T.dropWhileEnd isSpace <$> textP) <|> pure "" diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 83abdcf871..2d2504ee83 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -202,24 +202,22 @@ toggleNtf m ntfOn = withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchAllErrors` eToView prepareGroupMsg :: DB.Connection -> User -> GroupInfo -> Maybe MsgScope -> ShowGroupAsSender -> MsgContent -> Map MemberName MsgMention -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> ExceptT StoreError IO (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup)) -prepareGroupMsg db user g@GroupInfo {membership} msgScope showGroupAsSender mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = case (quotedItemId_, itemForwarded) of - (Nothing, Nothing) -> - let mc' = MCSimple $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) msgScope (justTrue showGroupAsSender) - in pure (XMsgNew mc', Nothing) - (Nothing, Just _) -> - let mc' = MCForward $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) msgScope (justTrue showGroupAsSender) - in pure (XMsgNew mc', Nothing) - (Just quotedItemId, Nothing) -> do - CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, mentions = quoteMentions, file} <- - getGroupCIWithReactions db user g quotedItemId - (origQmc, qd, sent, member_) <- quoteData qci membership - let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = memberId' <$> member_} - qmc = quoteContent mc origQmc file - (qmc', ft', _) = updatedMentionNames qmc formattedText quoteMentions - quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc', formattedText = ft'} - mc' = MCQuote QuotedMsg {msgRef, content = qmc'} (ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) msgScope (justTrue showGroupAsSender)) - pure (XMsgNew mc', Just quotedItem) - (Just _, Just _) -> throwError SEInvalidQuote +prepareGroupMsg db user g@GroupInfo {membership} msgScope showGroupAsSender mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = do + (mc', quotedItem_) <- case (quotedItemId_, itemForwarded) of + (Nothing, Nothing) -> pure (mcSimple mc, Nothing) + (Nothing, Just _) -> pure (mcForward mc, Nothing) + (Just quotedItemId, Nothing) -> do + CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, mentions = quoteMentions, file} <- + getGroupCIWithReactions db user g quotedItemId + (origQmc, qd, sent, member_) <- quoteData qci membership + let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = memberId' <$> member_} + qmc = quoteContent mc origQmc file + (qmc', ft', _) = updatedMentionNames qmc formattedText quoteMentions + quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc', formattedText = ft'} + pure (mcQuote QuotedMsg {msgRef, content = qmc'} mc, Just quotedItem) + (Just _, Just _) -> throwError SEInvalidQuote + let mc'' = mc' {mentions = MsgMentions mentions, file = fInv_, ttl = ttl' <$> timed_, live = justTrue live, scope = msgScope, asGroup = justTrue showGroupAsSender} + pure (XMsgNew mc'', quotedItem_) where quoteData :: ChatItem c d -> GroupMember -> ExceptT StoreError IO (MsgContent, CIQDirection 'CTGroup, Bool, Maybe GroupMember) quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} _ = throwError SEInvalidQuote @@ -340,12 +338,17 @@ quoteContent mc qmc ciFile_ prohibitedGroupContent :: GroupInfo -> GroupMember -> Maybe GroupChatScopeInfo -> MsgContent -> Maybe MarkdownList -> Maybe f -> Bool -> Maybe GroupFeature prohibitedGroupContent gInfo@GroupInfo {membership = mem@GroupMember {memberRole = userRole}} m scopeInfo mc ft file_ sent + | not supportAllowed = Just GFSupport | isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) && not hostApprovalVoice = Just GFVoice | isNothing scopeInfo && not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles | isNothing scopeInfo && isReport mc && (badReportUser || not (groupFeatureAllowed SGFReports gInfo)) = Just GFReports - | isNothing scopeInfo && prohibitedSimplexLinks gInfo m ft = Just GFSimplexLinks + | isNothing scopeInfo && prohibitedSimplexLinks gInfo m mc ft = Just GFSimplexLinks | otherwise = Nothing where + supportAllowed = case scopeInfo of + Just (GCSIMemberSupport scopeMem_) -> + groupFeatureAllowed SGFSupport gInfo || isJust (supportChat $ fromMaybe mem scopeMem_) + Nothing -> True hostApprovalVoice | sent = userRole >= GRAdmin && sendApprovalPhase | otherwise = memberCategory m == GCHostMember && hostApprovalPhase @@ -360,10 +363,14 @@ prohibitedGroupContent gInfo@GroupInfo {membership = mem@GroupMember {memberRole | sent = userRole >= GRModerator | otherwise = userRole < GRModerator -prohibitedSimplexLinks :: GroupInfo -> GroupMember -> Maybe MarkdownList -> Bool -prohibitedSimplexLinks gInfo m ft = +prohibitedSimplexLinks :: GroupInfo -> GroupMember -> MsgContent -> Maybe MarkdownList -> Bool +prohibitedSimplexLinks gInfo m mc ft = not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo) - && maybe False (any ftIsSimplexLink) ft + && (isChatLink mc || maybe False (any ftIsSimplexLink) ft) + where + isChatLink = \case + MCChat {} -> True + _ -> False ftIsSimplexLink :: FormattedText -> Bool ftIsSimplexLink FormattedText {format} = maybe False isSimplexLink format @@ -1026,7 +1033,7 @@ acceptBusinessJoinRequestAsync createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode let cd = CDGroupSnd gInfo Nothing -- TODO [short links] move to profileContactRequest? - createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing + createInternalChatItem user cd (CISndGroupE2EEInfo $ e2eInfoGroup gInfo) Nothing createGroupFeatureItems user cd CISndGroupFeature gInfo -- TODO [short links] get updated business chat group and member? (currently not used) pure (gInfo, clientMember) @@ -1061,7 +1068,7 @@ introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRol let msg = if maxVersion (memberChatVRange m) >= groupKnockingVersion then XGrpLinkAcpt GAPendingReview memberRole memberId - else XMsgNew $ MCSimple $ extMsgContent (MCText pendingReviewMessage) Nothing + else XMsgNew $ mcSimple (MCText pendingReviewMessage) void $ sendDirectMemberMessage mConn msg groupId modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo let rcpModMs = filter shouldIntroduceToMod modMs @@ -1194,9 +1201,12 @@ sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just c where descrEvent_ :: Maybe (ChatMsgEvent 'Json) descrEvent_ + -- in channels sendHistory runs on the relay, which cannot author XMsgNew (GRRelay < GRObserver); + -- the welcome message reaches new members via the channel link data instead + | useRelays' gInfo = Nothing | m `supportsVersion` groupHistoryIncludeWelcomeVersion = do let GroupInfo {groupProfile = GroupProfile {description}} = gInfo - fmap (\descr -> XMsgNew $ MCSimple $ extMsgContent (MCText descr) Nothing) description + fmap (\descr -> XMsgNew $ mcSimple (MCText descr)) description | otherwise = Nothing itemForwardEvents :: CChatItem 'CTGroup -> CM [ChatMsgEvent 'Json] itemForwardEvents cci = case cci of @@ -1298,7 +1308,8 @@ setGroupLinkData nm user gInfo gLink = do (conn, groupRelays) <- withFastStore $ \db -> (,) <$> getGroupLinkConnection db vr user gInfo <*> liftIO (getConnectedGroupRelays db gInfo) let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays - sLnk <- shortenShortLink' . toShortGroupLink =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData (Just crClientData)) + linkType = if useRelays' gInfo then CCTChannel else CCTGroup + sLnk <- shortenShortLink' . setShortLinkType_ linkType =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData (Just crClientData)) withFastStore' $ \db -> setGroupLinkShortLink db gLink sLnk setGroupLinkDataAsync :: User -> GroupInfo -> GroupLink -> CM () @@ -1321,29 +1332,58 @@ updatePublicGroupData user gInfo pure gInfo' | otherwise = pure gInfo +updateGroupFromLinkData :: User -> GroupInfo -> GroupShortLinkData -> CM (GroupInfo, Bool) +updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} GroupShortLinkData {groupProfile, publicGroupData} + | profileChanged || countChanged = do + vr <- chatVersionRange + withStore $ \db -> do + g <- if profileChanged then updateGroupProfile db user gInfo groupProfile else pure gInfo + g' <- case publicGroupData of + Just PublicGroupData {publicMemberCount} | countChanged -> + setPublicMemberCount db vr user g publicMemberCount + _ -> pure g + pure (g', profileChanged) + | otherwise = pure (gInfo, False) + where + profileChanged = p /= groupProfile + countChanged = case publicGroupData of + Just PublicGroupData {publicMemberCount} -> Just publicMemberCount /= localCount + _ -> False + -- TODO [relays] owner: set owners on updating link data (multi-owner) groupLinkData :: GroupInfo -> GroupLink -> [GroupRelay] -> (UserConnLinkData 'CMContact, CRClientData) -groupLinkData gInfo@GroupInfo {groupProfile, groupSummary = GroupSummary {publicMemberCount}} GroupLink {groupLinkId} groupRelays = +groupLinkData gInfo@GroupInfo {groupProfile, groupSummary = GroupSummary {publicMemberCount}, membership = GroupMember {memberId}, groupKeys} GroupLink {groupLinkId} groupRelays = let direct = not $ useRelays' gInfo relays = mapMaybe (\GroupRelay {relayLink} -> relayLink) groupRelays publicGroupData_ = PublicGroupData <$> publicMemberCount userData = encodeShortLinkData $ GroupShortLinkData {groupProfile, publicGroupData = publicGroupData_} - userLinkData = UserContactLinkData UserContactData {direct, owners = [], relays, userData} + owners = case groupKeys of + Just GroupKeys {groupRootKey = GRKPrivate rootPrivKey, memberPrivKey} -> + let ownerId = unMemberId memberId + ownerKey = C.publicKey memberPrivKey + authOwnerSig = C.sign' rootPrivKey (ownerId <> C.encodePubKey ownerKey) + in [OwnerAuth {ownerId, ownerKey, authOwnerSig}] + _ -> [] + userLinkData = UserContactLinkData UserContactData {direct, owners, relays, userData} crClientData = encodeJSON $ CRDataGroup groupLinkId in (userLinkData, crClientData) restoreShortLink' :: ConnShortLink m -> CM (ConnShortLink m) restoreShortLink' l = (`restoreShortLink` l) <$> asks (shortLinkPresetServers . config) -getShortLinkConnReq :: NetworkRequestMode -> User -> ConnShortLink m -> CM (FixedLinkData m, ConnLinkData m) -getShortLinkConnReq nm user@User {userChatRelay} l = do +getShortLinkConnReq' :: NetworkRequestMode -> User -> ConnShortLink m -> CM (FixedLinkData m, ConnLinkData m) +getShortLinkConnReq' nm user l = do l' <- restoreShortLink' l - (fd, cData) <- withAgent $ \a -> getConnShortLink a nm (aUserId user) l' + withAgent $ \a -> getConnShortLink a nm (aUserId user) l' + +getShortLinkConnReq :: NetworkRequestMode -> User -> ConnShortLink m -> CM (FixedLinkData m, ConnLinkData m) +getShortLinkConnReq nm user l = do + (fd, cData) <- getShortLinkConnReq' nm user l case cData of ContactLinkData _ UserContactData {direct, relays} | not supported -> throwChatError CEUnsupportedConnReq where - supported = direct || not (null relays) || isTrue userChatRelay + supported = direct || not (null relays) _ -> pure () pure (fd, cData) @@ -1377,27 +1417,6 @@ shortenShortLink' l = (`shortenShortLink` l) <$> asks (shortLinkPresetServers . shortenCreatedLink :: CreatedConnLink m -> CM (CreatedConnLink m) shortenCreatedLink (CCLink cReq sLnk) = CCLink cReq <$> mapM shortenShortLink' sLnk -createdGroupLink :: CreatedLinkContact -> CreatedLinkContact -createdGroupLink (CCLink cReq shortLink) = CCLink cReq (toShortGroupLink <$> shortLink) - -toShortGroupLink :: ShortLinkContact -> ShortLinkContact -toShortGroupLink (CSLContact sch _ srv k) = CSLContact sch CCTGroup srv k - -createdChannelLink :: CreatedLinkContact -> CreatedLinkContact -createdChannelLink (CCLink cReq shortLink) = CCLink cReq (toShortChannelLink <$> shortLink) - -toShortChannelLink :: ShortLinkContact -> ShortLinkContact -toShortChannelLink (CSLContact sch _ srv k) = CSLContact sch CCTChannel srv k - -createdRelayLink :: CreatedLinkContact -> CreatedLinkContact -createdRelayLink (CCLink cReq shortLink) = CCLink cReq (toShortRelayLink <$> shortLink) - -toShortRelayLink :: ShortLinkContact -> ShortLinkContact -toShortRelayLink (CSLContact sch _ srv k) = CSLContact sch CCTRelay srv k - -toShortLinkContact :: CreatedLinkContact -> Maybe ShortLinkContact -toShortLinkContact (CCLink _cReq sLink) = sLink - deleteGroupLink' :: User -> GroupInfo -> CM () deleteGroupLink' user gInfo = do vr <- chatVersionRange @@ -1470,7 +1489,7 @@ createContactPQSndItem :: User -> Contact -> Connection -> PQEncryption -> CM (C createContactPQSndItem user ct conn@Connection {pqSndEnabled} pqSndEnabled' = flip catchAllErrors (const $ pure (ct, conn)) $ case (pqSndEnabled, pqSndEnabled') of (Just b, b') | b' /= b -> createPQItem $ CISndConnEvent (SCEPqEnabled pqSndEnabled') - (Nothing, PQEncOn) -> createPQItem $ CISndDirectE2EEInfo (E2EInfo $ Just pqSndEnabled') + (Nothing, PQEncOn) -> createPQItem $ CISndDirectE2EEInfo (e2eInfoEncrypted $ Just pqSndEnabled') _ -> pure (ct, conn) where createPQItem ciContent = do @@ -1485,7 +1504,7 @@ updateContactPQRcv :: User -> Contact -> Connection -> PQEncryption -> CM (Conta updateContactPQRcv user ct conn@Connection {connId, pqRcvEnabled} pqRcvEnabled' = flip catchAllErrors (const $ pure (ct, conn)) $ case (pqRcvEnabled, pqRcvEnabled') of (Just b, b') | b' /= b -> updatePQ $ CIRcvConnEvent (RCEPqEnabled pqRcvEnabled') - (Nothing, PQEncOn) -> updatePQ $ CIRcvDirectE2EEInfo (E2EInfo $ Just pqRcvEnabled') + (Nothing, PQEncOn) -> updatePQ $ CIRcvDirectE2EEInfo (e2eInfoEncrypted $ Just pqRcvEnabled') _ -> pure (ct, conn) where updatePQ ciContent = do @@ -1788,9 +1807,12 @@ deleteOrUpdateMemberRecord user gInfo m = deleteOrUpdateMemberRecordIO :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO GroupInfo deleteOrUpdateMemberRecordIO db user@User {userId} gInfo m = do (gInfo', m') <- deleteSupportChatIfExists db user gInfo m - checkGroupMemberHasItems db user m' >>= \case - Just _ -> updateGroupMemberStatus db userId m' GSMemRemoved - Nothing -> deleteGroupMember db user m' + if isRelay m' + then deleteGroupMember db user m' + else + checkGroupMemberHasItems db user m' >>= \case + Just _ -> updateGroupMemberStatus db userId m' GSMemRemoved + Nothing -> deleteGroupMember db user m' pure gInfo' updateMemberRecordDeleted :: User -> GroupInfo -> GroupMember -> GroupMemberStatus -> CM GroupInfo @@ -1798,8 +1820,15 @@ updateMemberRecordDeleted user@User {userId} gInfo m newStatus = withStore' $ \db -> do (gInfo', m') <- deleteSupportChatIfExists db user gInfo m updateGroupMemberStatus db userId m' newStatus + deactivateRelay_ db m pure gInfo' +deactivateRelay_ :: DB.Connection -> GroupMember -> IO () +deactivateRelay_ db m = + when (isRelay m) $ do + relay_ <- runExceptT $ getGroupRelayByGMId db (groupMemberId' m) + forM_ relay_ $ \relay -> void $ updateRelayStatus db relay RSInactive + deleteSupportChatIfExists :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO (GroupInfo, GroupMember) deleteSupportChatIfExists db user gInfo m = do gInfo' <- diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 5e6d3dd326..57dc737e2d 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -37,7 +37,7 @@ import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) -import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime) +import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, diffUTCTime, getCurrentTime) import qualified Data.UUID as UUID import qualified Data.UUID.V4 as V4 import Data.Word (Word32) @@ -77,7 +77,7 @@ import Simplex.Messaging.Agent.Client (getAgentWorker, temporaryOrHostError, wai import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), Worker (..)) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) -import Simplex.Messaging.Agent.RetryInterval (withRetryInterval) +import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..), nextRetryDelay) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Client (NetworkRequestMode (..), ProxyClientError (..)) import qualified Simplex.Messaging.Crypto as C @@ -94,8 +94,9 @@ import Simplex.Messaging.Transport (TransportError (..)) import Simplex.Messaging.Util import Simplex.Messaging.Version import qualified System.FilePath as FP +import System.Mem.Weak (Weak) import Text.Read (readMaybe) -import UnliftIO.Concurrent (forkIO) +import UnliftIO.Concurrent (ThreadId, forkIO, mkWeakThreadId) import UnliftIO.Directory import UnliftIO.STM @@ -502,7 +503,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Left e -> do atomically $ modifyTVar' tags ("error" :) logInfo $ "contact msg=error " <> eInfo <> " " <> tshow e - eToView (ChatError . CEException $ "error parsing chat message: " <> e) + createInternalChatItem user (CDDirectRcv ct') (CIRcvMsgError $ RMEParseError $ T.pack e) Nothing + `catchAllErrors` \_ -> pure () withRcpt <- checkSendRcpt ct' $ rights aChatMsgs -- not crucial to use ct'' from processEvent pure (withRcpt, False) where @@ -598,7 +600,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- [incognito] print incognito profile used for this contact incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) toView $ CEvtContactConnected user ct' (fmap fromLocalProfile incognitoProfile) - let createE2EItem = createInternalChatItem user (CDDirectRcv ct') (CIRcvDirectE2EEInfo $ E2EInfo $ Just pqEnc) Nothing + let createE2EItem = createInternalChatItem user (CDDirectRcv ct') (CIRcvDirectE2EEInfo $ e2eInfoEncrypted $ Just pqEnc) Nothing -- TODO [short links] get contact request by contactRequestId, check encryption (UserContactRequest.pqSupport)? when (directOrUsed ct') $ case (preparedContact ct', contactRequestId' ct') of (Nothing, Nothing) -> do @@ -695,6 +697,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- error cannot be AUTH error here updateDirectItemsStatusMsgs ct conn (L.toList msgIds) (CISSndError $ agentSndError err) eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity) + ERR (AGENT (A_DUPLICATE (Just DroppedMsg {brokerTs, attempts}))) -> + createInternalChatItem user (CDDirectRcv ct) (CIRcvMsgError $ RMEDropped attempts) (Just brokerTs) ERR err -> do eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () @@ -705,7 +709,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Just UserContactRequest {welcomeSharedMsgId = Just smId} -> void $ sendDirectContactMessage user ct $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing Nothing _ -> do - (msg, _) <- sendDirectContactMessage user ct $ XMsgNew $ MCSimple $ extMsgContent mc Nothing + (msg, _) <- sendDirectContactMessage user ct $ XMsgNew $ mcSimple mc ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] @@ -846,7 +850,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = firstConnectedHost ( do let cd = CDGroupRcv gInfo'' scopeInfo m'' - createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing + createInternalChatItem user cd (CIRcvGroupE2EEInfo $ e2eInfoGroup gInfo'') Nothing let prepared = preparedGroup gInfo'' unless (isJust prepared) $ createGroupFeatureItems user cd CIRcvGroupFeature gInfo'' memberConnectedChatItem gInfo'' scopeInfo m'' @@ -934,7 +938,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = newDeliveryTasks <- reverse <$> foldM (processAChatMsg gInfo' scopeInfo m' tags eInfo) [] aChatMsgs shouldDelConns <- if isUserGrpFwdRelay gInfo' && not (blockedByAdmin m) - then createDeliveryTasks gInfo' m' newDeliveryTasks + then + let tasks + | relayOwnStatus gInfo' == Just RSInactive = filter relayRemovedNewTask newDeliveryTasks + | otherwise = newDeliveryTasks + in createDeliveryTasks gInfo' m' tasks else pure False withRcpt <- checkSendRcpt $ rights aChatMsgs pure (withRcpt, shouldDelConns) @@ -969,7 +977,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Left e -> do atomically $ modifyTVar' tags ("error" :) logInfo $ "group msg=error " <> eInfo <> " " <> tshow e - eToView (ChatError . CEException $ "error parsing chat message: " <> e) + if isRelay membership + then + eToView (ChatError . CEException $ "error parsing chat message: " <> e) + else + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m') (CIRcvMsgError $ RMEParseError $ T.pack e) Nothing + `catchAllErrors` \_ -> pure () pure newDeliveryTasks processEvent :: forall e. MsgEncodingI e => GroupInfo -> GroupMember -> VerifiedMsg e -> CM (Maybe NewMessageDeliveryTask) processEvent gInfo' m' verifiedMsg = do @@ -986,7 +999,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = checkSendAsGroup asGroup $ memberCanSend (Just m'') scope $ newGroupContentMessage gInfo' (Just m'') mc msg brokerTs False where - ExtMsgContent {scope, asGroup} = mcExtMsgContent mc + MsgContainer {scope, asGroup} = mc -- file description is always allowed, to allow sending files to support scope XMsgFileDescr sharedMsgId fileDescr -> groupMessageFileDescription gInfo' (Just m'') sharedMsgId fileDescr XMsgUpdate sharedMsgId mContent mentions ttl live msgScope asGroup_ -> @@ -1037,6 +1050,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = where aChatMsgHasReceipt (APMsg _ (ParsedMsg _ _ ChatMessage {chatMsgEvent})) = hasDeliveryReceipt (toCMEventTag chatMsgEvent) + relayRemovedNewTask :: NewMessageDeliveryTask -> Bool + relayRemovedNewTask NewMessageDeliveryTask {taskContext = DeliveryTaskContext {jobScope}} = isRelayRemoved jobScope createDeliveryTasks :: GroupInfo -> GroupMember -> [NewMessageDeliveryTask] -> CM ShouldDeleteGroupConns createDeliveryTasks gInfo'@GroupInfo {groupId = gId} m' newDeliveryTasks = do let relayRemovedTask_ = find (\NewMessageDeliveryTask {taskContext = DeliveryTaskContext {jobScope}} -> isRelayRemoved jobScope) newDeliveryTasks @@ -1184,6 +1199,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> forM_ msgIds $ \msgId -> updateGroupItemsErrorStatus db msgId (groupMemberId' m) newStatus `catchAll_` pure () eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity) + ERR err@(AGENT (A_DUPLICATE (Just DroppedMsg {brokerTs, attempts}))) + | isRelay membership -> + eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity) + | otherwise -> do + (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m') (CIRcvMsgError $ RMEDropped attempts) (Just brokerTs) ERR err -> do eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () @@ -1209,7 +1230,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Just UserContactRequest {welcomeSharedMsgId = Just smId} -> void $ sendGroupMessage' user gInfo [m] $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing Nothing _ -> do - msg <- sendGroupMessage' user gInfo [m] $ XMsgNew $ MCSimple $ extMsgContent mc Nothing + msg <- sendGroupMessage' user gInfo [m] $ XMsgNew $ mcSimple mc ci <- saveSndChatItem user (CDGroupSnd gInfo Nothing) msg (CISndMsgContent mc) withStore' $ \db -> createGroupSndStatus db (chatItemId' ci) (groupMemberId' m) GSSNew toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo Nothing) ci] @@ -1297,8 +1318,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = pure (gInfo, gLink, relays', changed) toView $ CEvtGroupLinkDataUpdated user gInfo gLink relays relaysChanged where - -- TODO [relays] owner: on relay deletion (link absent from relayLinks) - -- TODO move status RSActive to new "Removed" status / remove relay record updateRelay :: DB.Connection -> GroupRelay -> ([GroupRelay], Bool) -> IO ([GroupRelay], Bool) updateRelay db relay@GroupRelay {relayLink, relayStatus} (acc, changed) = case relayLink of @@ -1306,6 +1325,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | rLink `elem` relayLinks && relayStatus == RSAccepted -> do relay' <- updateRelayStatus db relay RSActive pure (relay' : acc, True) + | rLink `elem` relayLinks -> pure (relay : acc, changed) + | relayStatus == RSActive -> do + -- Relay link absent from link data — deactivate. + -- RSAccepted relays are not deactivated: their own link data update + -- may not have been processed yet (race with concurrent relay connections). + -- TODO [relays] multi-owner: Another owner removing a relay updates link data on + -- TODO the SMP server, but this owner won't receive a LINK callback for it + -- TODO (LINK only fires in response to own setConnShortLink calls). + relay' <- updateRelayStatus db relay RSInactive + pure (relay' : acc, True) _ -> pure (relay : acc, changed) _ -> throwChatError $ CECommandError "LINK event expected for a group link only" _ -> throwChatError $ CECommandError "unexpected cmdFunction" @@ -1348,7 +1377,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = upsertDirectRequestItem cd (requestMsg_, prevSharedMsgId_) Nothing -> do void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart) - let e2eContent = CIRcvDirectE2EEInfo $ E2EInfo $ Just $ CR.pqSupportToEnc $ reqPQSup + let e2eContent = CIRcvDirectE2EEInfo $ e2eInfoEncrypted $ Just $ CR.pqSupportToEnc $ reqPQSup void $ createChatItem user cd False e2eContent Nothing Nothing void $ createFeatureEnabledItems_ user ct forM_ (autoReply addressSettings) $ \mc -> forM_ welcomeSharedMsgId $ \sharedMsgId -> @@ -1484,7 +1513,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason xGrpRelayInv :: InvitationId -> VersionRangeChat -> GroupRelayInvitation -> CM () xGrpRelayInv invId chatVRange groupRelayInv = do - (_gInfo, _ownerMember) <- withStore $ \db -> createRelayRequestGroup db vr user groupRelayInv invId chatVRange + initialDelay <- asks $ initialInterval . relayRequestRetryInterval . config + (_gInfo, _ownerMember) <- withStore $ \db -> createRelayRequestGroup db vr user groupRelayInv invId chatVRange initialDelay lift $ void $ getRelayRequestWorker True xGrpRelayTest :: InvitationId -> VersionRangeChat -> ByteString -> CM () xGrpRelayTest invId chatVRange challenge = do @@ -1518,12 +1548,18 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = muteEventInChannel :: GroupInfo -> GroupMember -> Bool muteEventInChannel gInfo@GroupInfo {membership} m = - useRelays' gInfo && memberRole' membership < GRModerator && not (isRelay membership) && memberRole' m < GRModerator + useRelays' gInfo + && not (isRelay membership) -- relay users see all events + && not (isRelay m) -- relay events (e.g. leave) are visible to all + && memberRole' membership < GRModerator + && memberRole' m < GRModerator memberCanSend :: Maybe GroupMember -> Maybe MsgScope -> CM (Maybe DeliveryTaskContext) -> CM (Maybe DeliveryTaskContext) memberCanSend Nothing _ a = a -- channel message - was previously checked and allowed by relay memberCanSend (Just m@GroupMember {memberRole}) msgScope a = case msgScope of - Just MSMember {} -> a + Just (MSMember mId) + | sameMemberId mId m || memberRole >= GRModerator -> a + | otherwise -> messageError "member is not allowed to send to this support chat" $> Nothing Nothing | memberRole > GRObserver || memberPending m -> a | otherwise -> messageError "member is not allowed to send messages" $> Nothing @@ -1718,7 +1754,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> CM () newContentMessage ct mc msg@RcvMessage {sharedMsgId_} msgMeta = do - let ExtMsgContent content _ fInv_ _ _ _ _ = mcExtMsgContent mc + let MsgContainer {content = c, file = fInv_} = mc + content <- case c of + MCChat {text, chatLink, ownerSig = Just LinkOwnerSig {chatBinding = B64UrlByteString binding}} -> do + keepSig <- case contactConn ct of + Nothing -> pure False + Just conn -> do + adHash <- withAgent (`getConnectionRatchetAdHash` aConnId conn) + pure $ encodeChatBinding CBDirect adHash == binding + pure $ if keepSig then c else MCChat {text, chatLink, ownerSig = Nothing} + _ -> pure c -- Uncomment to test stuck delivery on errors - see test testDirectMessageDelete -- case content of -- MCText "hello 111" -> @@ -1729,7 +1774,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = then do void $ newChatItem (ciContentNoParse $ CIRcvChatFeatureRejected CFVoice) Nothing Nothing False else do - let ExtMsgContent _ _ _ itemTTL live_ _ _ = mcExtMsgContent mc + let MsgContainer {ttl = itemTTL, live = live_} = mc timed_ = rcvContactCITimed ct itemTTL live = fromMaybe False live_ file_ <- processFileInvitation fInv_ content $ \db -> createRcvFileTransfer db userId ct @@ -1816,13 +1861,19 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- This patches initial sharedMsgId into chat item when locally deleted chat item -- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete). -- Chat item and update message which created it will have different sharedMsgId in this case... - let timed_ = rcvContactCITimed ct ttl - ts = ciContentTexts content - (ci, cInfo) <- saveRcvChatItem' user (CDDirectRcv ct) msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live M.empty - ci' <- withStore' $ \db -> do - createChatItemVersion db (chatItemId' ci) brokerTs mc - updateDirectChatItem' db user contactId ci content True live Nothing Nothing - toView $ CEvtChatItemUpdated user (AChatItem SCTDirect SMDRcv cInfo ci') + if isVoice mc && not (featureAllowed SCFVoice forContact ct) + then do + let ciContent = ciContentNoParse $ CIRcvChatFeatureRejected CFVoice + (ci, cInfo) <- saveRcvChatItem' user (CDDirectRcv ct) msg (Just sharedMsgId) brokerTs ciContent Nothing Nothing False M.empty + toView $ CEvtChatItemUpdated user (AChatItem SCTDirect SMDRcv cInfo ci) + else do + let timed_ = rcvContactCITimed ct ttl + ts = ciContentTexts content + (ci, cInfo) <- saveRcvChatItem' user (CDDirectRcv ct) msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live M.empty + ci' <- withStore' $ \db -> do + createChatItemVersion db (chatItemId' ci) brokerTs mc + updateDirectChatItem' db user contactId ci content True live Nothing Nothing + toView $ CEvtChatItemUpdated user (AChatItem SCTDirect SMDRcv cInfo ci') where brokerTs = metaBrokerTs msgMeta content = CIRcvMsgContent mc @@ -1971,7 +2022,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = rejected gInfo' m' scopeInfo f = newChatItem gInfo' m' scopeInfo (ciContentNoParse $ CIRcvGroupFeatureRejected f) Nothing Nothing False timed_ gInfo' = if forwarded then rcvCITimed_ (Just Nothing) itemTTL else rcvGroupCITimed gInfo' itemTTL live' = fromMaybe False live_ - ExtMsgContent content mentions fInv_ itemTTL live_ msgScope_ asGroup_ = mcExtMsgContent mc + MsgContainer {content = c, mentions = MsgMentions mentions, file = fInv_, ttl = itemTTL, live = live_, scope = msgScope_, asGroup = asGroup_} = mc + content = case c of + MCChat {text, chatLink, ownerSig = Just LinkOwnerSig {chatBinding = B64UrlByteString binding}} -> case publicGroup of + Just pgp | maybe False (binding ==) (expectedBinding pgp) -> c + _ -> MCChat {text, chatLink, ownerSig = Nothing} + _ -> c + expectedBinding PublicGroupProfile {publicGroupId} + | sentAsGroup = Just $ encodeChatBinding CBChannel (smpEncode publicGroupId) + | otherwise = (\GroupMember {memberId} -> encodeChatBinding CBGroup (smpEncode (publicGroupId, memberId))) <$> m_ + GroupInfo {groupProfile = GroupProfile {publicGroup}} = gInfo sentAsGroup = asGroup_ == Just True ts@(_, ft_) = msgContentTexts content -- m' is Maybe GroupMember @@ -2013,7 +2073,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let fileMember_ = if sentAsGroup then Nothing else m' in processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId gInfo' fileMember_ newChatItem gInfo' m' scopeInfo ciContent ciFile_ timed live = do - let mentions' = if maybe False memberBlocked m' then [] else mentions + let mentions' = if maybe False memberBlocked m' then M.empty else mentions (ci, cInfo) <- saveRcvCI gInfo' m' scopeInfo ciContent ciFile_ timed live mentions' ci' <- maybe (pure ci) (\m -> blockedMemberCI gInfo' m ci) m' let memberId_ = memberId' <$> m' @@ -2022,7 +2082,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = groupMessageUpdate :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> MsgContent -> Map MemberName MsgMention -> Maybe MsgScope -> RcvMessage -> UTCTime -> Maybe Int -> Maybe Bool -> Maybe Bool -> CM (Maybe DeliveryTaskContext) groupMessageUpdate gInfo@GroupInfo {groupId} m_ sharedMsgId mc mentions msgScope_ msg@RcvMessage {msgId} brokerTs ttl_ live_ asGroup_ - | Just m <- m_, prohibitedSimplexLinks gInfo m ft_ = + | Just m <- m_, prohibitedSimplexLinks gInfo m mc ft_ = messageWarning ("x.msg.update ignored: feature not allowed " <> groupFeatureNameText GFSimplexLinks) $> Nothing | otherwise = do updateRcvChatItem `catchCINotFound` \_ -> do @@ -2043,15 +2103,22 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (gInfo', m', scopeInfo) <- mkGetMessageChatScope vr user gInfo m mc msgScope_ pure (gInfo', CDGroupRcv gInfo' scopeInfo m', mentions', scopeInfo) Nothing -> pure (gInfo, CDChannelRcv gInfo Nothing, mentions, Nothing) - (ci, cInfo) <- saveRcvChatItem' user chatDir msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live mentions' - ci' <- withStore' $ \db -> do - createChatItemVersion db (chatItemId' ci) brokerTs mc - updateGroupChatItem db user groupId ci content True live Nothing - ci'' <- case chatDir of - CDGroupRcv gi' _ m' -> blockedMemberCI gi' m' ci' - CDChannelRcv {} -> pure ci' - toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv cInfo ci'') - pure $ Just $ infoToDeliveryContext gInfo' scopeInfo showGroupAsSender + case m_ >>= \m -> prohibitedGroupContent gInfo' m scopeInfo mc ft_ (Nothing :: Maybe String) False of + Just f -> do + let ciContent = ciContentNoParse $ CIRcvGroupFeatureRejected f + (ci, cInfo) <- saveRcvChatItem' user chatDir msg (Just sharedMsgId) brokerTs ciContent Nothing timed_ False M.empty + groupMsgToView cInfo ci + pure Nothing + Nothing -> do + (ci, cInfo) <- saveRcvChatItem' user chatDir msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live mentions' + ci' <- withStore' $ \db -> do + createChatItemVersion db (chatItemId' ci) brokerTs mc + updateGroupChatItem db user groupId ci content True live Nothing + ci'' <- case chatDir of + CDGroupRcv gi' _ m' -> blockedMemberCI gi' m' ci' + CDChannelRcv {} -> pure ci' + toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv cInfo ci'') + pure $ Just $ infoToDeliveryContext gInfo' scopeInfo showGroupAsSender where content = CIRcvMsgContent mc ts@(_, ft_) = msgContentTexts mc @@ -2516,7 +2583,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- create item in both scopes let gInfo' = gInfo {membership = membership'} cd = CDGroupRcv gInfo' Nothing m - createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing + createInternalChatItem user cd (CIRcvGroupE2EEInfo $ e2eInfoGroup gInfo') Nothing let prepared = preparedGroup gInfo' unless (isJust prepared) $ createGroupFeatureItems user cd CIRcvGroupFeature gInfo' let welcomeMsgId_ = (\PreparedGroup {welcomeSharedMsgId = mId} -> mId) <$> preparedGroup gInfo' @@ -2586,10 +2653,18 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = BCCustomer -> customerId == memberId createProfileUpdatedItem m' (msg, brokerTs) = do (gInfo', m'', scopeInfo) <- mkGroupChatScope gInfo m' - let ciContent = CIRcvGroupEvent $ RGEMemberProfileUpdated (fromLocalProfile p) p' - cd = CDGroupRcv gInfo' scopeInfo m'' - (ci, cInfo) <- saveRcvChatItemNoParse user cd msg brokerTs ciContent - groupMsgToView cInfo ci + let createItem scopeInfo_ m_ = do + let ciContent = CIRcvGroupEvent $ RGEMemberProfileUpdated (fromLocalProfile p) p' + cd = CDGroupRcv gInfo' scopeInfo_ m_ + (ci, cInfo) <- saveRcvChatItemNoParse user cd msg brokerTs ciContent + groupMsgToView cInfo ci + case scopeInfo of + Just _ -> createItem scopeInfo m'' + Nothing + | useRelays' gInfo' && not (isRelay m'') && memberRole' m'' < GRModerator -> + forM_ (supportChat m'') $ \_ -> + createItem (Just GCSIMemberSupport {groupMember_ = Just m''}) m'' + | otherwise -> createItem Nothing m'' xInfoProbe :: ContactOrMember -> Probe -> CM () xInfoProbe cgm2 probe = do @@ -2887,13 +2962,19 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = GCHostMember -> withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case Right existingMember - | useRelays' gInfo -> - void $ withStore $ \db -> updatePreparedChannelMember db vr user existingMember memInfo + | useRelays' gInfo -> do + updatedMember <- withStore $ \db -> updatePreparedChannelMember db vr user existingMember memInfo + toView $ CEvtGroupMemberUpdated user gInfo existingMember updatedMember | otherwise -> messageError "x.grp.mem.intro ignored: member already exists" Left _ - | useRelays' gInfo -> - void $ withStore $ \db -> createIntroReMember db user gInfo memInfo memRestrictions + | useRelays' gInfo -> do + -- owner key must only come from link data, not from relay intro + let memInfo' = case memInfo of + MemberInfo mId mRole v p _ + | mRole == GROwner -> MemberInfo mId mRole v p Nothing + _ -> memInfo + void $ withStore $ \db -> createIntroReMember db user gInfo memInfo' memRestrictions | otherwise -> do when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole c) case memChatVRange of @@ -3035,10 +3116,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = deleteGroupLinkIfExists user gInfo -- TODO [relays] possible improvement is to immediately delete rcv queues if isUserGrpFwdRelay unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections user gInfo False - withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemRemoved + withStore' $ \db -> do + updateGroupMemberStatus db userId membership GSMemRemoved + when (isJust $ relayOwnStatus gInfo) $ updateRelayOwnStatus_ db gInfo RSInactive let membership' = membership {memberStatus = GSMemRemoved} when withMessages $ deleteMessages gInfo membership' SMDSnd - deleteMemberItem gInfo RGEUserDeleted + deleteMemberItem msg gInfo RGEUserDeleted toView $ CEvtDeletedMemberUser user gInfo {membership = membership'} m withMessages msgSigned pure $ Just DJSGroup {jobSpec = DJRelayRemoved} else @@ -3066,7 +3149,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let wasDeleted = memberStatus == GSMemRemoved || memberStatus == GSMemLeft deletedMember' = deletedMember {memberStatus = GSMemRemoved} when withMessages $ deleteMessages gInfo'' deletedMember' SMDRcv - unless wasDeleted $ deleteMemberItem gInfo'' $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile) + -- Clear forwardedByMember if it references the deleted member, + -- as the member record was already deleted above. + let RcvMessage {forwardedByMember = fwdBy} = msg + msg' = if fwdBy == Just groupMemberId then (msg :: RcvMessage) {forwardedByMember = Nothing} else msg + unless wasDeleted $ deleteMemberItem msg' gInfo'' $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile) toView $ CEvtDeletedMember user gInfo'' m deletedMember' withMessages msgSigned pure deliveryScope where @@ -3074,9 +3161,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | senderRole < GRAdmin || senderRole < memberRole = messageError "x.grp.mem.del with insufficient member permissions" $> Nothing | otherwise = a - deleteMemberItem gi gEvent = do + deleteMemberItem msg' gi gEvent = do (gi', m', scopeInfo) <- mkGroupChatScope gi m - (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gi' scopeInfo m') msg brokerTs (CIRcvGroupEvent gEvent) + (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gi' scopeInfo m') msg' brokerTs (CIRcvGroupEvent gEvent) groupMsgToView cInfo ci deleteMessages :: MsgDirectionI d => GroupInfo -> GroupMember -> SMsgDirection d -> CM () deleteMessages gInfo' delMem msgDir @@ -3144,7 +3231,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (ci, cInfo) <- saveRcvChatItemNoParse user cd msg brokerTs (CIRcvGroupEvent $ RGEGroupUpdated p') groupMsgToView cInfo ci createGroupFeatureChangedItems user cd CIRcvGroupFeature g g'' - void $ forkIO $ void $ setGroupLinkData' NRMBackground user g'' + -- in channels, link data is updated by the owner making the change in runUpdateGroupProfile; + -- other owners receiving the update do not refresh the same link + unless (useRelays' g'') $ + void $ forkIO $ void $ setGroupLinkData' NRMBackground user g'' Just _ -> updateGroupPrefs_ msgSigned g m $ fromMaybe defaultBusinessGroupPrefs $ groupPreferences p' pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = True}} @@ -3274,7 +3364,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = XMsgNew mc -> void $ memberCanSend author_ scope $ newGroupContentMessage gInfo author_ mc rcvMsg msgTs True where - ExtMsgContent {scope} = mcExtMsgContent mc + MsgContainer {scope} = mc -- file description is always allowed, to allow sending files to support scope XMsgFileDescr sharedMsgId fileDescr -> void $ groupMessageFileDescription gInfo author_ sharedMsgId fileDescr XMsgUpdate sharedMsgId mContent mentions ttl live msgScope asGroup_ -> @@ -3458,19 +3548,24 @@ runDeliveryTaskWorker a deliveryKey Worker {doWork} = do processDeliveryTask :: MessageDeliveryTask -> CM () processDeliveryTask task@MessageDeliveryTask {jobScope} = case jobScopeImpliedSpec jobScope of - DJDeliveryJob _includePending -> - withWorkItems a doWork (withStore' $ \db -> getNextDeliveryTasks db gInfo task) $ \nextTasks -> do - let (body, taskIds, largeTaskIds) = batchDeliveryTasks1 vr maxEncodedMsgLength nextTasks - withStore' $ \db -> do - createMsgDeliveryJob db gInfo jobScope (singleSenderGMId_ nextTasks) body - forM_ taskIds $ \taskId -> updateDeliveryTaskStatus db taskId DTSProcessed - forM_ largeTaskIds $ \taskId -> setDeliveryTaskErrStatus db taskId "large" - lift . void $ getDeliveryJobWorker True deliveryKey + DJDeliveryJob _includePending + | relayOwnStatus gInfo == Just RSInactive -> do + logWarn "delivery task worker: relay inactive" + withStore' $ \db -> setDeliveryTaskErrStatus db (deliveryTaskId task) "relay inactive" + | otherwise -> + withWorkItems a doWork (withStore' $ \db -> getNextDeliveryTasks db gInfo task) $ \nextTasks -> do + let (body, taskIds, largeTaskIds) = batchDeliveryTasks1 vr maxEncodedMsgLength nextTasks + withStore' $ \db -> do + createMsgDeliveryJob db gInfo jobScope (singleSenderGMId_ nextTasks) body + forM_ taskIds $ \taskId -> updateDeliveryTaskStatus db taskId DTSProcessed + forM_ largeTaskIds $ \taskId -> setDeliveryTaskErrStatus db taskId "large" + lift . void $ getDeliveryJobWorker True deliveryKey where singleSenderGMId_ :: NonEmpty MessageDeliveryTask -> Maybe GroupMemberId singleSenderGMId_ (MessageDeliveryTask {senderGMId = senderGMId'} :| ts) | all (\MessageDeliveryTask {senderGMId} -> senderGMId == senderGMId') ts = Just senderGMId' | otherwise = Nothing + -- DJRelayRemoved is allowed when RSInactive - it forwards XGrpMemDel about relay's own deletion DJRelayRemoved | workerScope /= DWSGroup -> throwChatError $ CEInternalError "delivery task worker: relay removed task in wrong worker scope" @@ -3523,9 +3618,14 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do processDeliveryJob :: MessageDeliveryJob -> CM () processDeliveryJob job = case jobScopeImpliedSpec jobScope of - DJDeliveryJob _includePending -> do - sendBodyToMembers - withStore' $ \db -> updateDeliveryJobStatus db jobId DJSComplete + DJDeliveryJob _includePending + | relayOwnStatus gInfo == Just RSInactive -> do + logWarn "delivery job worker: relay inactive" + withStore' $ \db -> setDeliveryJobErrStatus db (deliveryJobId job) "relay inactive" + | otherwise -> do + sendBodyToMembers + withStore' $ \db -> updateDeliveryJobStatus db jobId DJSComplete + -- DJRelayRemoved is allowed when RSInactive - it forwards XGrpMemDel about relay's own deletion DJRelayRemoved | workerScope /= DWSGroup -> throwChatError $ CEInternalError "delivery job worker: relay removed job in wrong worker scope" @@ -3644,23 +3744,55 @@ runRelayRequestWorker a Worker {doWork} = do user <- getRelayUser db UserContactLink {userContactLinkId} <- getUserAddress db user pure (user, userContactLinkId) + delayThreads <- liftIO TM.emptyIO forever $ do lift $ waitForWork doWork - runRelayRequestOperation vr user uclId + runRelayRequestOperation delayThreads vr user uclId where - runRelayRequestOperation :: VersionRangeChat -> User -> Int64 -> CM () - runRelayRequestOperation vr user uclId = - withWork_ a doWork (withStore' getNextPendingRelayRequest) $ + runRelayRequestOperation :: TM.TMap GroupId (TMVar (Weak ThreadId)) -> VersionRangeChat -> User -> Int64 -> CM () + runRelayRequestOperation delayThreads vr user uclId = + withWork_ a doWork getReadyRelayRequest $ \(groupId, rrd) -> do - ri <- asks $ reconnectInterval . agentConfig . config - withRetryInterval ri $ \_ loop -> do - liftIO $ waitWhileSuspended a - liftIO $ waitForUserNetwork a - processRelayRequest groupId rrd `catchAllErrors` retryTmpError loop groupId + ChatConfig {relayRequestExpiry} <- asks config + liftIO $ waitWhileSuspended a + liftIO $ waitForUserNetwork a + processRelayRequest groupId rrd `catchAllErrors` retryTmpError relayRequestExpiry groupId rrd where - retryTmpError :: CM () -> GroupId -> ChatError -> CM () - retryTmpError loop groupId = \case - ChatErrorAgent {agentError} | temporaryOrHostError agentError -> loop + getReadyRelayRequest :: CM (Either StoreError (Maybe (GroupId, RelayRequestData))) + getReadyRelayRequest = + withStore' getNextPendingRelayRequest >>= \case + Right (Just (groupId, rrd@RelayRequestData {reqExecuteAt})) -> do + currentTs <- liftIO getCurrentTime + let delay = diffUTCTime reqExecuteAt currentTs + if delay <= 1 + then pure $ Right (Just (groupId, rrd)) + else Right Nothing <$ scheduleRequest groupId delay + r -> pure r + scheduleRequest :: GroupId -> NominalDiffTime -> CM () + scheduleRequest groupId delay = do + v_ <- liftIO $ atomically $ + ifM + (isNothing <$> TM.lookup groupId delayThreads) + (newEmptyTMVar >>= \v -> TM.insert groupId v delayThreads $> Just v) + (pure Nothing) + forM_ v_ $ \v -> do + tId <- liftIO $ forkIO $ do + threadDelay' $ diffToMicroseconds delay + atomically $ TM.delete groupId delayThreads + void $ atomically $ tryPutTMVar doWork () + weakTId <- liftIO $ mkWeakThreadId tId + liftIO $ atomically $ putTMVar v weakTId + retryTmpError :: (Int, NominalDiffTime) -> GroupId -> RelayRequestData -> ChatError -> CM () + retryTmpError (retriesThreshold, ttl) groupId RelayRequestData {reqDelay, reqRetries, reqCreatedAt} = \case + ChatErrorAgent {agentError} | temporaryOrHostError agentError -> do + currentTs <- liftIO getCurrentTime + if reqRetries >= retriesThreshold && diffUTCTime currentTs reqCreatedAt >= ttl + then withStore' $ \db -> setRelayRequestErr db groupId "expired" + else do + ri <- asks $ relayRequestRetryInterval . config + let executeAt = addUTCTime (fromIntegral reqDelay / 1000000) currentTs + nextDelay = nextRetryDelay 0 reqDelay ri + withStore' $ \db -> updateRelayRequestRetries db groupId nextDelay executeAt e -> do withStore' $ \db -> setRelayRequestErr db groupId (tshow e) eToView e @@ -3682,7 +3814,7 @@ runRelayRequestWorker a Worker {doWork} = do where getLinkDataCreateRelayLink :: RelayRequestData -> GroupInfo -> CM (GroupInfo, ShortLinkContact) getLinkDataCreateRelayLink RelayRequestData {reqGroupLink} gInfo = do - (FixedLinkData {linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners})) <- getShortLinkConnReq NRMBackground user reqGroupLink + (FixedLinkData {linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners})) <- getShortLinkConnReq' NRMBackground user reqGroupLink liftIO (decodeLinkUserData cData) >>= \case Nothing -> throwChatError $ CEException "getLinkDataCreateRelayLink: no group link data" Just GroupShortLinkData {groupProfile = gp@GroupProfile {publicGroup}} -> do @@ -3713,8 +3845,8 @@ runRelayRequestWorker a Worker {doWork} = do let crClientData = encodeJSON $ CRDataGroup groupLinkId -- prepare link with relayMemId as linkEntityId (no server request) (ccLink, preparedParams) <- withAgent $ \a' -> prepareConnectionLink a' (aUserId user) sigKeys relayMemId True (Just crClientData) - ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink - sLnk <- case toShortLinkContact ccLink' of + ccLink' <- setShortLinkType CCTGroup <$> shortenCreatedLink ccLink + sLnk <- case connShortLink' ccLink' of Just sl -> pure sl Nothing -> throwChatError $ CEException "failed to create relay link: no short link" let userData = encodeShortLinkData $ RelayShortLinkData {relayProfile = fromLocalProfile p} diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index e404388d8d..5800ab5bdd 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -119,6 +119,11 @@ checkChatType x = case testEquality (chatTypeI @c) (chatTypeI @c') of data GroupChatScope = GCSMemberSupport {groupMemberId_ :: Maybe GroupMemberId} -- Nothing means own conversation with support deriving (Eq, Show, Ord) +sendAsGroup' :: GroupInfo -> Maybe GroupChatScope -> Bool +sendAsGroup' gInfo@GroupInfo {membership} scope = case scope of + Nothing -> useRelays' gInfo && memberRole' membership == GROwner + Just (GCSMemberSupport _) -> False + data GroupChatScopeTag = GCSTMemberSupport_ deriving (Eq, Show) @@ -1308,11 +1313,6 @@ data CIForwardedFrom | CIFFGroup {chatName :: Text, msgDir :: MsgDirection, groupId :: Maybe GroupId, chatItemId :: Maybe ChatItemId} deriving (Show) -cmForwardedFrom :: AChatMsgEvent -> Maybe CIForwardedFrom -cmForwardedFrom = \case - ACME _ (XMsgNew (MCForward _)) -> Just CIFFUnknown - _ -> Nothing - data CIForwardedFromTag = CIFFUnknown_ | CIFFContact_ diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index e2e878033d..2dc751d6bb 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -145,6 +145,7 @@ data CIContent (d :: MsgDirection) where CIRcvCall :: CICallStatus -> Int -> CIContent 'MDRcv CIRcvIntegrityError :: MsgErrorType -> CIContent 'MDRcv CIRcvDecryptionError :: MsgDecryptError -> Word32 -> CIContent 'MDRcv + CIRcvMsgError :: RcvMsgError -> CIContent 'MDRcv CIRcvGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDRcv CISndGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDSnd CIRcvDirectEvent :: RcvDirectEvent -> CIContent 'MDRcv @@ -176,9 +177,16 @@ data CIContent (d :: MsgDirection) where deriving instance Show (CIContent d) -data E2EInfo = E2EInfo {pqEnabled :: Maybe PQEncryption} +-- stored in database, all changed must be backward compatible +data E2EInfo = E2EInfo {public :: Maybe Bool, pqEnabled :: Maybe PQEncryption} deriving (Eq, Show) +e2eInfoEncrypted :: Maybe PQEncryption -> E2EInfo +e2eInfoEncrypted pqEnabled = E2EInfo {public = Nothing, pqEnabled} + +e2eInfoGroup :: GroupInfo -> E2EInfo +e2eInfoGroup g = E2EInfo {public = if useRelays' g then Just True else Nothing, pqEnabled = Just PQEncOff} + ciMsgContent :: CIContent d -> Maybe MsgContent ciMsgContent = \case CISndMsgContent mc -> Just mc @@ -196,6 +204,11 @@ data MsgDecryptError | MDERatchetSync deriving (Eq, Show) +data RcvMsgError + = RMEDropped {attempts :: Int} + | RMEParseError {parseError :: Text} + deriving (Eq, Show) + ciRequiresAttention :: forall d. MsgDirectionI d => CIContent d -> Bool ciRequiresAttention content = case msgDirection @d of SMDSnd -> True @@ -205,6 +218,7 @@ ciRequiresAttention content = case msgDirection @d of CIRcvCall {} -> True CIRcvIntegrityError _ -> True CIRcvDecryptionError {} -> True + CIRcvMsgError _ -> False CIRcvGroupInvitation {} -> True CIRcvDirectEvent rde -> case rde of RDEContactDeleted -> False @@ -275,6 +289,7 @@ ciContentToText = \case CIRcvCall status duration -> "incoming call: " <> ciCallInfoText status duration CIRcvIntegrityError err -> msgIntegrityError err CIRcvDecryptionError err n -> msgDecryptErrorText err n + CIRcvMsgError err -> rcvMsgErrorText err CIRcvGroupInvitation groupInvitation memberRole -> "received " <> ciGroupInvitationToText groupInvitation memberRole CISndGroupInvitation groupInvitation memberRole -> "sent " <> ciGroupInvitationToText groupInvitation memberRole CIRcvDirectEvent event -> rcvDirectEventToText event @@ -307,9 +322,14 @@ directE2EInfoToText E2EInfo {pqEnabled} = case pqEnabled of Nothing -> simpleE2EText groupE2EInfoToText :: E2EInfo -> Text -groupE2EInfoToText E2EInfo {pqEnabled} = case pqEnabled of - Just _ -> e2eInfoNoPQText - Nothing -> simpleE2EText +groupE2EInfoToText E2EInfo {pqEnabled, public} = case public of + Just True -> publicGroupNoE2EText + _ -> case pqEnabled of + Just _ -> e2eInfoNoPQText + Nothing -> simpleE2EText + +publicGroupNoE2EText :: Text +publicGroupNoE2EText = "This channel or group is NOT end-to-end encrypted." simpleE2EText :: Text simpleE2EText = "This conversation is protected by end-to-end encryption" @@ -421,6 +441,11 @@ msgIntegrityError = \case MsgBadHash -> "incorrect message hash" MsgDuplicate -> "duplicate message ID" +rcvMsgErrorText :: RcvMsgError -> Text +rcvMsgErrorText = \case + RMEDropped {attempts} -> "message removed after " <> tshow attempts <> " attempts" + RMEParseError {parseError} -> "message error: " <> parseError + msgDecryptErrorText :: MsgDecryptError -> Word32 -> Text msgDecryptErrorText err n = "decryption error, possibly due to the device change" @@ -457,6 +482,7 @@ data JSONCIContent | JCIRcvCall {status :: CICallStatus, duration :: Int} | JCIRcvIntegrityError {msgError :: MsgErrorType} | JCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32} + | JCIRcvMsgError {rcvMsgError :: RcvMsgError} | JCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} | JCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} | JCIRcvDirectEvent {rcvDirectEvent :: RcvDirectEvent} @@ -492,6 +518,7 @@ jsonCIContent = \case CIRcvCall status duration -> JCIRcvCall {status, duration} CIRcvIntegrityError err -> JCIRcvIntegrityError err CIRcvDecryptionError err n -> JCIRcvDecryptionError err n + CIRcvMsgError err -> JCIRcvMsgError err CIRcvGroupInvitation groupInvitation memberRole -> JCIRcvGroupInvitation {groupInvitation, memberRole} CISndGroupInvitation groupInvitation memberRole -> JCISndGroupInvitation {groupInvitation, memberRole} CIRcvDirectEvent rcvDirectEvent -> JCIRcvDirectEvent {rcvDirectEvent} @@ -527,6 +554,7 @@ aciContentJSON = \case JCIRcvCall {status, duration} -> ACIContent SMDRcv $ CIRcvCall status duration JCIRcvIntegrityError err -> ACIContent SMDRcv $ CIRcvIntegrityError err JCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n + JCIRcvMsgError err -> ACIContent SMDRcv $ CIRcvMsgError err JCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole JCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole JCIRcvDirectEvent {rcvDirectEvent} -> ACIContent SMDRcv $ CIRcvDirectEvent rcvDirectEvent @@ -563,6 +591,7 @@ data DBJSONCIContent | DBJCIRcvCall {status :: CICallStatus, duration :: Int} | DBJCIRcvIntegrityError {msgError :: DBMsgErrorType} | DBJCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32} + | DBJCIRcvMsgError {rcvMsgError :: RcvMsgError} | DBJCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} | DBJCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} | DBJCIRcvDirectEvent {rcvDirectEvent :: DBRcvDirectEvent} @@ -598,6 +627,7 @@ dbJsonCIContent = \case CIRcvCall status duration -> DBJCIRcvCall {status, duration} CIRcvIntegrityError err -> DBJCIRcvIntegrityError $ DBME err CIRcvDecryptionError err n -> DBJCIRcvDecryptionError err n + CIRcvMsgError err -> DBJCIRcvMsgError err CIRcvGroupInvitation groupInvitation memberRole -> DBJCIRcvGroupInvitation {groupInvitation, memberRole} CISndGroupInvitation groupInvitation memberRole -> DBJCISndGroupInvitation {groupInvitation, memberRole} CIRcvDirectEvent rde -> DBJCIRcvDirectEvent $ RDE rde @@ -633,6 +663,7 @@ aciContentDBJSON = \case DBJCIRcvCall {status, duration} -> ACIContent SMDRcv $ CIRcvCall status duration DBJCIRcvIntegrityError (DBME err) -> ACIContent SMDRcv $ CIRcvIntegrityError err DBJCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n + DBJCIRcvMsgError err -> ACIContent SMDRcv $ CIRcvMsgError err DBJCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole DBJCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole DBJCIRcvDirectEvent (RDE rde) -> ACIContent SMDRcv $ CIRcvDirectEvent rde @@ -693,6 +724,8 @@ $(JQ.deriveJSON defaultJSON ''E2EInfo) $(JQ.deriveJSON (enumJSON $ dropPrefix "MDE") ''MsgDecryptError) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RME") ''RcvMsgError) + $(JQ.deriveJSON (enumJSON $ dropPrefix "CIGIS") ''CIGroupInvitationStatus) $(JQ.deriveJSON defaultJSON ''CIGroupInvitation) @@ -751,6 +784,7 @@ toCIContentTag ciContent = case ciContent of CIRcvCall {} -> "rcvCall" CIRcvIntegrityError _ -> "rcvIntegrityError" CIRcvDecryptionError {} -> "rcvDecryptionError" + CIRcvMsgError _ -> "rcvMsgError" CIRcvGroupInvitation {} -> "rcvGroupInvitation" CISndGroupInvitation {} -> "sndGroupInvitation" CIRcvDirectEvent _ -> "rcvDirectEvent" diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 3bbfb02d0a..03cc38e12a 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -60,10 +60,10 @@ import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util (atomicModifyIORef'_, safeDecodeUtf8) usageConditionsCommit :: Text -usageConditionsCommit = "7471fd2af5838dc0467aebc570b5ea75e5df3209" +usageConditionsCommit = "05f99634c470f8bddac20046947a0606938b22ad" previousConditionsCommit :: Text -previousConditionsCommit = "a5061f3147165a05979d6ace33960aced2d6ac03" +previousConditionsCommit = "7471fd2af5838dc0467aebc570b5ea75e5df3209" usageConditionsText :: Text usageConditionsText = diff --git a/src/Simplex/Chat/Operators/Presets.hs b/src/Simplex/Chat/Operators/Presets.hs index 23c9157121..53f31e005d 100644 --- a/src/Simplex/Chat/Operators/Presets.hs +++ b/src/Simplex/Chat/Operators/Presets.hs @@ -93,7 +93,8 @@ disabledSimplexChatSMPServers = simplexChatRelays :: [NewUserChatRelay] simplexChatRelays = [ presetChatRelay True (mkRelayProfile "SimpleX Chat Relay 1" $ Just simplexChatImage) ["simplex.im"] (either error id $ strDecode "https://smp5.simplex.im/r#Fp5RWXkiRFg-hgcDwC2v-MWnPfvEf42RgCqREntW0mw"), - presetChatRelay True (mkRelayProfile "SimpleX Chat Relay 2" $ Just simplexChatImage) ["simplex.im"] (either error id $ strDecode "https://smp6.simplex.im/r#_qlQfogHGDJ8MAF2wKmkglRBM-xHR142gDJstKiGRQQ") + presetChatRelay True (mkRelayProfile "SimpleX Chat Relay 2" $ Just simplexChatImage) ["simplex.im"] (either error id $ strDecode "https://smp6.simplex.im/r#_qlQfogHGDJ8MAF2wKmkglRBM-xHR142gDJstKiGRQQ"), + presetChatRelay True (mkRelayProfile "SimpleX Chat Relay 3" $ Just simplexChatImage) ["simplex.im"] (either error id $ strDecode "https://smp4.simplex.im/r#yxNOMJcry5jMTRPEBVtGBATYaKeoRIsZRBPIDLx7x6M") ] fluxSMPServers :: [NewUserServer 'PSMP] diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 6d7a094430..b7e838c52b 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -56,7 +56,7 @@ import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion) import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder, fromTextField_) -import Simplex.Messaging.Compression (Compressed, compress1, decompress1) +import Simplex.Messaging.Compression (Compressed, compress1, decompress1, decompressedSize) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -318,7 +318,7 @@ data AChatMessage = forall e. MsgEncodingI e => ACMsg (SMsgEncoding e) (ChatMess data KeyRef = KRMember deriving (Eq, Show) -data ChatBinding = CBGroup +data ChatBinding = CBGroup | CBDirect | CBChannel deriving (Eq, Show) data MsgSignature = MsgSignature KeyRef C.ASignature @@ -381,10 +381,15 @@ instance Encoding KeyRef where c -> fail $ "invalid KeyRef tag: " <> show c instance Encoding ChatBinding where - smpEncode CBGroup = "G" + smpEncode = \case + CBGroup -> "G" + CBDirect -> "D" + CBChannel -> "C" smpP = A.anyChar >>= \case 'G' -> pure CBGroup + 'D' -> pure CBDirect + 'C' -> pure CBChannel c -> fail $ "invalid ChatBinding: " <> show c instance ToField ChatBinding where toField = toField . decodeLatin1 . smpEncode @@ -411,7 +416,8 @@ data MsgSigning = MsgSigning privKey :: C.PrivateKeyEd25519 } - +encodeChatBinding :: ChatBinding -> ByteString -> ByteString +encodeChatBinding cb bindingData = smpEncode cb <> bindingData data ChatMsgEvent (e :: MsgEncoding) where XMsgNew :: MsgContainer -> ChatMsgEvent 'Json @@ -477,8 +483,8 @@ deriving instance Show AChatMsgEvent -- actual filtering on forwarding is done in processEvent isForwardedGroupMsg :: ChatMsgEvent e -> Bool isForwardedGroupMsg ev = case ev of - XMsgNew mc -> case mcExtMsgContent mc of - ExtMsgContent {file = Just FileInvitation {fileInline = Just _}} -> False + XMsgNew mc -> case mc of + MsgContainer {file = Just FileInvitation {fileInline = Just _}} -> False _ -> True XMsgFileDescr _ _ -> True XMsgUpdate {} -> True @@ -582,7 +588,7 @@ data QuotedMsg = QuotedMsg {msgRef :: MsgRef, content :: MsgContent} cmToQuotedMsg :: AChatMsgEvent -> Maybe QuotedMsg cmToQuotedMsg = \case - ACME _ (XMsgNew (MCQuote quotedMsg _)) -> Just quotedMsg + ACME _ (XMsgNew MsgContainer {quote = Just quotedMsg}) -> Just quotedMsg _ -> Nothing data MsgContentTag @@ -631,24 +637,51 @@ instance FromField MsgContentTag where fromField = fromTextField_ $ eitherToMayb instance ToField MsgContentTag where toField = toField . safeDecodeUtf8 . strEncode -data MsgContainer - = MCSimple ExtMsgContent - | MCQuote QuotedMsg ExtMsgContent - | MCComment MsgRef ExtMsgContent - | MCForward ExtMsgContent +-- Wire JSON 1:1 with parsed form. The three discriminator fields `quote`, `parent`, +-- and `forward` are independent and may co-occur (e.g. a comment that quotes another +-- comment carries both `parent` and `quote`). `forward` is `Maybe Bool` for backwards +-- compatibility with the previous wire encoding: the serializer omits the field when +-- `Nothing` and the parser treats absent/false as "not a forward". +data MsgContainer = MsgContainer + { content :: MsgContent, + -- the key used in mentions is a locally (per message) unique display name of member. + -- Suffixes _1, _2 should be appended to make names locally unique. + -- It should be done in the UI, as they will be part of the text, and validated in the API. + mentions :: MsgMentions, + file :: Maybe FileInvitation, + ttl :: Maybe Int, + live :: Maybe Bool, + scope :: Maybe MsgScope, + asGroup :: Maybe Bool, + quote :: Maybe QuotedMsg, + parent :: Maybe MsgRef, + forward :: Maybe Bool + } deriving (Eq, Show) -mcExtMsgContent :: MsgContainer -> ExtMsgContent -mcExtMsgContent = \case - MCSimple c -> c - MCQuote _ c -> c - MCComment _ c -> c - MCForward c -> c +mcSimple :: MsgContent -> MsgContainer +mcSimple content = + MsgContainer + { content, + mentions = MsgMentions M.empty, + file = Nothing, + ttl = Nothing, + live = Nothing, + scope = Nothing, + asGroup = Nothing, + quote = Nothing, + parent = Nothing, + forward = Nothing + } -isMCForward :: MsgContainer -> Bool -isMCForward = \case - MCForward _ -> True - _ -> False +mcQuote :: QuotedMsg -> MsgContent -> MsgContainer +mcQuote q c = (mcSimple c) {quote = Just q} + +mcComment :: MsgRef -> MsgContent -> MsgContainer +mcComment p c = (mcSimple c) {parent = Just p} + +mcForward :: MsgContent -> MsgContainer +mcForward c = (mcSimple c) {forward = Just True} data MsgContent = MCText {text :: Text} @@ -658,7 +691,7 @@ data MsgContent | MCVoice {text :: Text, duration :: Int} | MCFile {text :: Text} | MCReport {text :: Text, reason :: ReportReason} - | MCChat {text :: Text, chatLink :: MsgChatLink} + | MCChat {text :: Text, chatLink :: MsgChatLink, ownerSig :: Maybe LinkOwnerSig} | MCUnknown {tag :: Text, text :: Text, json :: J.Object} deriving (Eq, Show) @@ -668,6 +701,13 @@ data MsgChatLink | MCLGroup {connLink :: ShortLinkContact, groupProfile :: GroupProfile} deriving (Eq, Show) +data LinkOwnerSig = LinkOwnerSig + { ownerId :: Maybe B64UrlByteString, + chatBinding :: B64UrlByteString, + ownerSig :: C.Signature 'C.Ed25519 + } + deriving (Eq, Show) + msgContentText :: MsgContent -> Text msgContentText = \case MCText t -> t @@ -722,29 +762,100 @@ msgContentTag = \case MCChat {} -> MCChat_ MCUnknown {tag} -> MCUnknown_ tag -data ExtMsgContent = ExtMsgContent - { content :: MsgContent, - -- the key used in mentions is a locally (per message) unique display name of member. - -- Suffixes _1, _2 should be appended to make names locally unique. - -- It should be done in the UI, as they will be part of the text, and validated in the API. - mentions :: Map MemberName MsgMention, - file :: Maybe FileInvitation, - ttl :: Maybe Int, - live :: Maybe Bool, - scope :: Maybe MsgScope, - asGroup :: Maybe Bool - } +data MsgMention = MsgMention {memberId :: MemberId} deriving (Eq, Show) -data MsgMention = MsgMention {memberId :: MemberId} +newtype MsgMentions = MsgMentions (Map MemberName MsgMention) deriving (Eq, Show) $(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "MCL") ''MsgChatLink) +$(JQ.deriveJSON defaultJSON ''LinkOwnerSig) + $(JQ.deriveJSON defaultJSON ''MsgMention) +instance FromJSON MsgMentions where + parseJSON v = MsgMentions <$> parseJSON v + omittedField = Just $ MsgMentions M.empty + +instance ToJSON MsgMentions where + toJSON (MsgMentions m) = toJSON $ toMaybeMap m + toEncoding (MsgMentions m) = toEncoding $ toMaybeMap m + omitField (MsgMentions m) = M.null m + +toMaybeMap :: Map k v -> Maybe (Map k v) +toMaybeMap m = if M.null m then Nothing else Just m +{-# INLINE toMaybeMap #-} + $(JQ.deriveJSON defaultJSON ''QuotedMsg) +instance FromJSON MsgContent where + parseJSON (J.Object v) = + v .: "type" >>= \case + MCText_ -> MCText <$> v .: "text" + MCLink_ -> do + text <- v .: "text" + preview <- v .: "preview" + pure MCLink {text, preview} + MCImage_ -> do + text <- v .: "text" + image <- v .: "image" + pure MCImage {text, image} + MCVideo_ -> do + text <- v .: "text" + image <- v .: "image" + duration <- v .: "duration" + pure MCVideo {text, image, duration} + MCVoice_ -> do + text <- v .: "text" + duration <- v .: "duration" + pure MCVoice {text, duration} + MCFile_ -> MCFile <$> v .: "text" + MCReport_ -> do + text <- v .: "text" + reason <- v .: "reason" + pure MCReport {text, reason} + MCChat_ -> do + text <- v .: "text" + chatLink <- v .: "chatLink" + ownerSig <- v .:? "ownerSig" + pure MCChat {text, chatLink, ownerSig} + MCUnknown_ tag -> do + text <- fromMaybe unknownMsgType <$> v .:? "text" + pure MCUnknown {tag, text, json = v} + parseJSON invalid = + JT.prependFailure "bad MsgContent, " (JT.typeMismatch "Object" invalid) + +unknownMsgType :: Text +unknownMsgType = "unknown message type" + +(.=?) :: ToJSON v => JT.Key -> Maybe v -> [(J.Key, J.Value)] -> [(J.Key, J.Value)] +key .=? value = maybe id ((:) . (key .=)) value + +instance ToJSON MsgContent where + toJSON = \case + MCUnknown {json} -> J.Object json + MCText t -> J.object ["type" .= MCText_, "text" .= t] + MCLink {text, preview} -> J.object ["type" .= MCLink_, "text" .= text, "preview" .= preview] + MCImage {text, image} -> J.object ["type" .= MCImage_, "text" .= text, "image" .= image] + MCVideo {text, image, duration} -> J.object ["type" .= MCVideo_, "text" .= text, "image" .= image, "duration" .= duration] + MCVoice {text, duration} -> J.object ["type" .= MCVoice_, "text" .= text, "duration" .= duration] + MCFile t -> J.object ["type" .= MCFile_, "text" .= t] + MCReport {text, reason} -> J.object ["type" .= MCReport_, "text" .= text, "reason" .= reason] + MCChat {text, chatLink, ownerSig} -> J.object $ ("ownerSig" .=? ownerSig) ["type" .= MCChat_, "text" .= text, "chatLink" .= chatLink] + toEncoding = \case + MCUnknown {json} -> JE.value $ J.Object json + MCText t -> J.pairs $ "type" .= MCText_ <> "text" .= t + MCLink {text, preview} -> J.pairs $ "type" .= MCLink_ <> "text" .= text <> "preview" .= preview + MCImage {text, image} -> J.pairs $ "type" .= MCImage_ <> "text" .= text <> "image" .= image + MCVideo {text, image, duration} -> J.pairs $ "type" .= MCVideo_ <> "text" .= text <> "image" .= image <> "duration" .= duration + MCVoice {text, duration} -> J.pairs $ "type" .= MCVoice_ <> "text" .= text <> "duration" .= duration + MCFile t -> J.pairs $ "type" .= MCFile_ <> "text" .= t + MCReport {text, reason} -> J.pairs $ "type" .= MCReport_ <> "text" .= text <> "reason" .= reason + MCChat {text, chatLink, ownerSig} -> J.pairs $ "type" .= MCChat_ <> "text" .= text <> "chatLink" .= chatLink <> maybe mempty ("ownerSig" .=) ownerSig + +$(JQ.deriveJSON defaultJSON ''MsgContainer) + -- this limit reserves space for metadata in forwarded messages -- 15780 (limit used for fileChunkSize) - 161 (x.grp.msg.forward overhead) = 15619, - 16 for block encryption ("rounded" to 15602) maxEncodedMsgLength :: Int @@ -799,7 +910,11 @@ parseChatMessages msg = case B.head msg of decodeCompressed :: ByteString -> [Either String AParsedMsg] decodeCompressed s = case smpDecode s of Left e -> [Left e] - Right (compressed :: L.NonEmpty Compressed) -> concatMap (either (\e -> [Left e]) parseUncompressed' . decompress1 maxDecompressedMsgLength) compressed + Right (compressed :: L.NonEmpty Compressed) -> case traverse decompressedSize compressed of + Nothing -> [Left "compressed size not specified"] + Just sizes + | sum sizes > maxDecompressedMsgLength -> [Left "decompressed size exceeds limit"] + | otherwise -> concatMap (either (\e -> [Left e]) parseUncompressed' . decompress1) compressed parseUncompressed' "" = [Left "empty string"] parseUncompressed' s = parseUncompressed (B.head s) s -- Binary batch format: '=' ( )* @@ -831,109 +946,14 @@ markCompressedBatch :: ByteString -> ByteString markCompressedBatch = B.cons 'X' {-# INLINE markCompressedBatch #-} -parseMsgContainer :: J.Object -> JT.Parser MsgContainer -parseMsgContainer v = - MCQuote <$> v .: "quote" <*> mc - <|> MCComment <$> v .: "parent" <*> mc - <|> (v .: "forward" >>= \f -> (if f then MCForward else MCSimple) <$> mc) - -- The support for arbitrary object in "forward" property is added to allow - -- forward compatibility with forwards that include public group links. - <|> (MCForward <$> ((v .: "forward" :: JT.Parser J.Object) *> mc)) - <|> MCSimple <$> mc - where - mc = do - content <- v .: "content" - file <- v .:? "file" - ttl <- v .:? "ttl" - live <- v .:? "live" - mentions <- fromMaybe M.empty <$> (v .:? "mentions") - scope <- v .:? "scope" - asGroup <- v .:? "asGroup" - pure ExtMsgContent {content, mentions, file, ttl, live, scope, asGroup} - -extMsgContent :: MsgContent -> Maybe FileInvitation -> ExtMsgContent -extMsgContent mc file = ExtMsgContent mc M.empty file Nothing Nothing Nothing Nothing - justTrue :: Bool -> Maybe Bool justTrue True = Just True justTrue False = Nothing -instance FromJSON MsgContent where - parseJSON (J.Object v) = - v .: "type" >>= \case - MCText_ -> MCText <$> v .: "text" - MCLink_ -> do - text <- v .: "text" - preview <- v .: "preview" - pure MCLink {text, preview} - MCImage_ -> do - text <- v .: "text" - image <- v .: "image" - pure MCImage {text, image} - MCVideo_ -> do - text <- v .: "text" - image <- v .: "image" - duration <- v .: "duration" - pure MCVideo {text, image, duration} - MCVoice_ -> do - text <- v .: "text" - duration <- v .: "duration" - pure MCVoice {text, duration} - MCFile_ -> MCFile <$> v .: "text" - MCReport_ -> do - text <- v .: "text" - reason <- v .: "reason" - pure MCReport {text, reason} - MCChat_ -> do - text <- v .: "text" - chatLink <- v .: "chatLink" - pure MCChat {text, chatLink} - MCUnknown_ tag -> do - text <- fromMaybe unknownMsgType <$> v .:? "text" - pure MCUnknown {tag, text, json = v} - parseJSON invalid = - JT.prependFailure "bad MsgContent, " (JT.typeMismatch "Object" invalid) - -unknownMsgType :: Text -unknownMsgType = "unknown message type" - -msgContainerJSON :: MsgContainer -> J.Object -msgContainerJSON = \case - MCQuote qm mc -> o $ ("quote" .= qm) : msgContent mc - MCComment ref mc -> o $ ("parent" .= ref) : msgContent mc - MCForward mc -> o $ ("forward" .= True) : msgContent mc - MCSimple mc -> o $ msgContent mc - where - o = JM.fromList - msgContent ExtMsgContent {content, mentions, file, ttl, live, scope, asGroup} = - ("file" .=? file) $ ("ttl" .=? ttl) $ ("live" .=? live) $ ("mentions" .=? nonEmptyMap mentions) $ ("scope" .=? scope) $ ("asGroup" .=? asGroup) ["content" .= content] - nonEmptyMap :: Map k v -> Maybe (Map k v) nonEmptyMap m = if M.null m then Nothing else Just m {-# INLINE nonEmptyMap #-} -instance ToJSON MsgContent where - toJSON = \case - MCUnknown {json} -> J.Object json - MCText t -> J.object ["type" .= MCText_, "text" .= t] - MCLink {text, preview} -> J.object ["type" .= MCLink_, "text" .= text, "preview" .= preview] - MCImage {text, image} -> J.object ["type" .= MCImage_, "text" .= text, "image" .= image] - MCVideo {text, image, duration} -> J.object ["type" .= MCVideo_, "text" .= text, "image" .= image, "duration" .= duration] - MCVoice {text, duration} -> J.object ["type" .= MCVoice_, "text" .= text, "duration" .= duration] - MCFile t -> J.object ["type" .= MCFile_, "text" .= t] - MCReport {text, reason} -> J.object ["type" .= MCReport_, "text" .= text, "reason" .= reason] - MCChat {text, chatLink} -> J.object ["type" .= MCChat_, "text" .= text, "chatLink" .= chatLink] - toEncoding = \case - MCUnknown {json} -> JE.value $ J.Object json - MCText t -> J.pairs $ "type" .= MCText_ <> "text" .= t - MCLink {text, preview} -> J.pairs $ "type" .= MCLink_ <> "text" .= text <> "preview" .= preview - MCImage {text, image} -> J.pairs $ "type" .= MCImage_ <> "text" .= text <> "image" .= image - MCVideo {text, image, duration} -> J.pairs $ "type" .= MCVideo_ <> "text" .= text <> "image" .= image <> "duration" .= duration - MCVoice {text, duration} -> J.pairs $ "type" .= MCVoice_ <> "text" .= text <> "duration" .= duration - MCFile t -> J.pairs $ "type" .= MCFile_ <> "text" .= t - MCReport {text, reason} -> J.pairs $ "type" .= MCReport_ <> "text" .= text <> "reason" .= reason - MCChat {text, chatLink} -> J.pairs $ "type" .= MCChat_ <> "text" .= text <> "chatLink" .= chatLink - instance ToField MsgContent where toField = toField . encodeJSON @@ -1250,7 +1270,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do opt key = JT.parseEither (.:? key) params msg :: CMEventTag 'Json -> Either String (ChatMsgEvent 'Json) msg = \case - XMsgNew_ -> XMsgNew <$> JT.parseEither parseMsgContainer params + XMsgNew_ -> XMsgNew <$> JT.parseEither parseJSON (J.Object params) XMsgFileDescr_ -> XMsgFileDescr <$> p "msgId" <*> p "fileDescr" XMsgUpdate_ -> do msgId' <- p "msgId" @@ -1323,9 +1343,6 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XOk_ -> pure XOk XUnknown_ t -> pure $ XUnknown t params -(.=?) :: ToJSON v => JT.Key -> Maybe v -> [(J.Key, J.Value)] -> [(J.Key, J.Value)] -key .=? value = maybe id ((:) . (key .=)) value - chatToAppMessage :: forall e. MsgEncodingI e => ChatMessage e -> AppMessage e chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @e of SBinary -> AMBinary AppMessageBinary {msgId = Nothing, tag = B.head $ strEncode tag, body = chatMsgBinaryToBody chatMsg} @@ -1336,7 +1353,9 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en o = JM.fromList params :: ChatMsgEvent 'Json -> J.Object params = \case - XMsgNew container -> msgContainerJSON container + XMsgNew mc -> case toJSON mc of + J.Object obj -> obj + _ -> JM.empty XMsgFileDescr msgId' fileDescr -> o ["msgId" .= msgId', "fileDescr" .= fileDescr] XMsgUpdate {msgId = msgId', content, mentions, ttl, live, scope, asGroup} -> o $ ("asGroup" .=? asGroup) $ ("ttl" .=? ttl) $ ("live" .=? live) $ ("scope" .=? scope) $ ("mentions" .=? nonEmptyMap mentions) ["msgId" .= msgId', "content" .= content] XMsgDel msgId' memberId scope -> o $ ("memberId" .=? memberId) $ ("scope" .=? scope) ["msgId" .= msgId'] diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index f0c2502ff8..89100ff890 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -79,7 +79,7 @@ minRemoteCtrlVersion = AppVersion [6, 5, 0, 12] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 4, 6, 0] +minRemoteHostVersion = AppVersion [6, 5, 0, 12] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 9128404244..60f898e52e 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -429,6 +429,14 @@ updatePreparedContactUser WHERE contact_profile_id = ? |] (newUserId, currentTs, profileId) + DB.execute + db + [sql| + UPDATE chat_items + SET user_id = ?, updated_at = ? + WHERE contact_id = ? + |] + (newUserId, currentTs, contactId) safeDeleteLDN db user oldLDN getContact db vr newUser contactId diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 93fdf1868a..cca929d950 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -94,6 +94,9 @@ module Simplex.Chat.Store.Groups setGroupInProgressDone, createRelayRequestGroup, updateRelayOwnStatusFromTo, + updateRelayOwnStatus_, + getRelayServedGroups, + getRelayInactiveGroups, createNewContactMemberAsync, createJoiningMember, getMemberJoinRequest, @@ -188,7 +191,7 @@ import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing) import Data.Ord (Down (..)) import Data.Text (Text) import qualified Data.Text as T -import Data.Time.Clock (UTCTime (..), getCurrentTime) +import Data.Time.Clock (NominalDiffTime, UTCTime (..), addUTCTime, getCurrentTime) import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Messages import Simplex.Chat.Operators @@ -687,6 +690,14 @@ updatePreparedGroupUser db vr user gInfo@GroupInfo {groupId, membership} hostMem WHERE group_profile_id IN (SELECT group_profile_id FROM groups WHERE group_id = ?) |] (newUserId, currentTs, groupId) + DB.execute + db + [sql| + UPDATE chat_items + SET user_id = ?, updated_at = ? + WHERE group_id = ? + |] + (newUserId, currentTs, groupId) safeDeleteLDN db user oldGroupLDN updateMembership GroupMember {groupMemberId = membershipId} currentTs = DB.execute @@ -1507,8 +1518,8 @@ setGroupInProgressDone db GroupInfo {groupId} = do "UPDATE groups SET creating_in_progress = 0, updated_at = ? WHERE group_id = ?" (currentTs, groupId) -createRelayRequestGroup :: DB.Connection -> VersionRangeChat -> User -> GroupRelayInvitation -> InvitationId -> VersionRangeChat -> ExceptT StoreError IO (GroupInfo, GroupMember) -createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMember, fromMemberProfile, relayMemberId, groupLink} invId reqChatVRange = do +createRelayRequestGroup :: DB.Connection -> VersionRangeChat -> User -> GroupRelayInvitation -> InvitationId -> VersionRangeChat -> Int64 -> ExceptT StoreError IO (GroupInfo, GroupMember) +createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMember, fromMemberProfile, relayMemberId, groupLink} invId reqChatVRange initialDelay = do currentTs <- liftIO getCurrentTime -- Create group with placeholder profile let Profile {displayName = fromMemberLDN} = fromMemberProfile @@ -1524,7 +1535,7 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe } (groupId, _groupLDN) <- createGroup_ db userId placeholderProfile Nothing Nothing True (Just RSInvited) Nothing currentTs -- Store relay request data for recovery - liftIO $ setRelayRequestData_ groupId + liftIO $ setRelayRequestData_ groupId currentTs ownerMemberId <- insertOwner_ currentTs groupId let relayMember = MemberIdRole relayMemberId GRRelay -- TODO [member keys] should relays use member keys? @@ -1533,7 +1544,7 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe g <- getGroupInfo db vr user groupId pure (g, ownerMember) where - setRelayRequestData_ groupId = + setRelayRequestData_ groupId currentTs = DB.execute db [sql| @@ -1541,12 +1552,15 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe SET relay_request_inv_id = ?, relay_request_group_link = ?, relay_request_peer_chat_min_version = ?, - relay_request_peer_chat_max_version = ? + relay_request_peer_chat_max_version = ?, + relay_request_delay = ?, + relay_request_execute_at = ? WHERE group_id = ? |] - (Binary invId, groupLink, minVersion reqChatVRange, maxVersion reqChatVRange, groupId) + (Binary invId, groupLink, minVersion reqChatVRange, maxVersion reqChatVRange, initialDelay, currentTs, groupId) insertOwner_ currentTs groupId = do let MemberIdRole {memberId, memberRole} = fromMember + VersionRange minV maxV = reqChatVRange (localDisplayName, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs indexInGroup <- getUpdateNextIndexInGroup_ db groupId liftIO $ do @@ -1555,11 +1569,13 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe [sql| INSERT INTO group_members ( group_id, index_in_group, member_id, member_role, member_category, member_status, - user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, + peer_chat_min_version, peer_chat_max_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (groupId, indexInGroup, memberId, memberRole, GCHostMember, GSMemAccepted) :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs) + :. (minV, maxV) ) insertedRowId db @@ -1572,7 +1588,29 @@ updateRelayOwnStatusFromTo db gInfo@GroupInfo {groupId} fromStatus toStatus = do updateRelayOwnStatus_ :: DB.Connection -> GroupInfo -> RelayStatus -> IO () updateRelayOwnStatus_ db GroupInfo {groupId} relayStatus = do currentTs <- getCurrentTime - DB.execute db "UPDATE groups SET relay_own_status = ?, updated_at = ? WHERE group_id = ?" (relayStatus, currentTs, groupId) + let inactiveAt_ = if relayStatus == RSInactive then Just currentTs else Nothing + DB.execute db "UPDATE groups SET relay_own_status = ?, relay_inactive_at = ?, updated_at = ? WHERE group_id = ?" (relayStatus, inactiveAt_, currentTs, groupId) + +getRelayServedGroups :: DB.Connection -> VersionRangeChat -> User -> IO [GroupInfo] +getRelayServedGroups db vr User {userId, userContactId} = do + map (toGroupInfo vr userContactId []) + <$> DB.query + db + ( groupInfoQuery + <> " WHERE g.user_id = ? AND mu.contact_id = ? AND g.relay_own_status IN (?, ?)" + ) + (userId, userContactId, RSAccepted, RSActive) + +getRelayInactiveGroups :: DB.Connection -> VersionRangeChat -> User -> NominalDiffTime -> IO [GroupInfo] +getRelayInactiveGroups db vr User {userId, userContactId} ttl = do + cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime + map (toGroupInfo vr userContactId []) + <$> DB.query + db + ( groupInfoQuery + <> " WHERE g.user_id = ? AND mu.contact_id = ? AND g.relay_own_status = ? AND g.relay_inactive_at IS NOT NULL AND g.relay_inactive_at <= ?" + ) + (userId, userContactId, RSInactive, cutoffTs) createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> SubscriptionMode -> ExceptT StoreError IO () createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) chatV peerChatVRange subMode = @@ -2958,8 +2996,8 @@ createNewUnknownGroupMember db vr user@User {userId, userContactId} GroupInfo {g where VersionRange minV maxV = vr -createLinkOwnerMember :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> C.PublicKeyEd25519 -> ExceptT StoreError IO GroupMember -createLinkOwnerMember db vr user@User {userId, userContactId} GroupInfo {groupId} memberId ownerKey = do +createLinkOwnerMember :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> Maybe ContactId -> MemberId -> C.PublicKeyEd25519 -> ExceptT StoreError IO GroupMember +createLinkOwnerMember db vr user@User {userId, userContactId} GroupInfo {groupId} contactId_ memberId ownerKey = do currentTs <- liftIO getCurrentTime let memberProfile = profileFromName $ nameFromMemberId memberId (localDisplayName, profileId) <- createNewMemberProfile_ db user memberProfile currentTs @@ -2975,7 +3013,7 @@ createLinkOwnerMember db vr user@User {userId, userContactId} GroupInfo {groupId VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (groupId, indexInGroup, memberId, GROwner, GCPreMember, GSMemUnknown, Binary B.empty, fromInvitedBy userContactId IBUnknown) - :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, ownerKey, currentTs, currentTs) + :. (userId, localDisplayName, contactId_, profileId, ownerKey, currentTs, currentTs) :. (minV, maxV) ) groupMemberId <- liftIO $ insertedRowId db diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index a2c91af86b..5d433088a4 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -229,7 +229,7 @@ createNewSndMessage db gVar connOrGroupId chatMsgEvent msgSigning_ encodeMessage ECMEncoded msgBody -> do let signedMsg_ = signBody <$> msgSigning_ signBody MsgSigning {bindingTag, bindingData, keyRef, privKey} = - let sig = C.ASignature C.SEd25519 $ C.sign' privKey (smpEncode bindingTag <> bindingData <> msgBody) + let sig = C.ASignature C.SEd25519 $ C.sign' privKey (encodeChatBinding bindingTag bindingData <> msgBody) in SignedMsg {chatBinding = bindingTag, signatures = MsgSignature keyRef sig :| [], signedBody = msgBody} createdAt <- getCurrentTime DB.execute @@ -240,7 +240,7 @@ createNewSndMessage db gVar connOrGroupId chatMsgEvent msgSigning_ encodeMessage shared_msg_id, shared_msg_id_user, created_at, updated_at ) VALUES (?,?,?,?,?,?,?,?,?,?,?) |] - ((MDSnd, toCMEventTag chatMsgEvent, DB.Binary msgBody, chatBinding <$> signedMsg_, DB.Binary . smpEncode . signatures <$> signedMsg_, connId_, groupId_) + ((MDSnd, toCMEventTag chatMsgEvent, DB.Binary msgBody, (\SignedMsg {chatBinding} -> chatBinding) <$> signedMsg_, DB.Binary . smpEncode . signatures <$> signedMsg_, connId_, groupId_) :. (DB.Binary sharedMsgId, Just (BI True), createdAt, createdAt)) msgId <- insertedRowId db pure $ Right SndMessage {msgId, sharedMsgId = SharedMsgId sharedMsgId, msgBody, signedMsg_} @@ -327,7 +327,7 @@ createNewRcvMessage db connOrGroupId NewRcvMessage {chatMsgEvent, verifiedMsg, b shared_msg_id, author_group_member_id, forwarded_by_group_member_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ((MDRcv, toCMEventTag chatMsgEvent, DB.Binary msgBody, chatBinding <$> signedMsg_, DB.Binary . smpEncode . signatures <$> signedMsg_, brokerTs, currentTs, currentTs, connId_, groupId_) + ((MDRcv, toCMEventTag chatMsgEvent, DB.Binary msgBody, (\SignedMsg {chatBinding} -> chatBinding) <$> signedMsg_, DB.Binary . smpEncode . signatures <$> signedMsg_, brokerTs, currentTs, currentTs, connId_, groupId_) :. (sharedMsgId_, authorMember, forwardedByMember)) msgId <- insertedRowId db pure RcvMessage {msgId, chatMsgEvent = ACME (encoding @e) chatMsgEvent, sharedMsgId_, msgSigned, forwardedByMember} @@ -558,7 +558,9 @@ createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, msgS quotedItem <- mapM (getChatItemQuote_ db user chatDirection) quotedMsg pure (ciId, quotedItem, itemForwarded) where - itemForwarded = cmForwardedFrom chatMsgEvent + itemForwarded = case chatMsgEvent of + ACME _ (XMsgNew MsgContainer {forward}) | forward == Just True -> Just CIFFUnknown + _ -> Nothing quotedMsg = cmToQuotedMsg chatMsgEvent quoteRow :: NewQuoteRow quoteRow = case quotedMsg of diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index c4812e75f4..608e3637c6 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -28,7 +28,9 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260108_chat_indices import Simplex.Chat.Store.Postgres.Migrations.M20260122_has_link import Simplex.Chat.Store.Postgres.Migrations.M20260222_chat_relays import Simplex.Chat.Store.Postgres.Migrations.M20260403_item_viewed -import Simplex.Chat.Store.Postgres.Migrations.M20260407_client_services +import Simplex.Chat.Store.Postgres.Migrations.M20260429_relay_request_retries +import Simplex.Chat.Store.Postgres.Migrations.M20260507_relay_inactive_at +import Simplex.Chat.Store.Postgres.Migrations.M20260520_client_services import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -57,7 +59,9 @@ schemaMigrations = ("20260122_has_link", m20260122_has_link, Just down_m20260122_has_link), ("20260222_chat_relays", m20260222_chat_relays, Just down_m20260222_chat_relays), ("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed), - ("20260407_client_services", m20260407_client_services, Just down_m20260407_client_services) + ("20260429_relay_request_retries", m20260429_relay_request_retries, Just down_m20260429_relay_request_retries), + ("20260507_relay_inactive_at", m20260507_relay_inactive_at, Just down_m20260507_relay_inactive_at) + ("20260520_client_services", m20260520_client_services, Just down_m20260520_client_services) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260429_relay_request_retries.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260429_relay_request_retries.hs new file mode 100644 index 0000000000..df9a2632f7 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260429_relay_request_retries.hs @@ -0,0 +1,23 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260429_relay_request_retries where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260429_relay_request_retries :: Text +m20260429_relay_request_retries = + [r| +ALTER TABLE groups ADD COLUMN relay_request_retries BIGINT NOT NULL DEFAULT 0; +ALTER TABLE groups ADD COLUMN relay_request_delay BIGINT NOT NULL DEFAULT 0; +ALTER TABLE groups ADD COLUMN relay_request_execute_at TIMESTAMPTZ NOT NULL DEFAULT '1970-01-01 00:00:00+00'; +|] + +down_m20260429_relay_request_retries :: Text +down_m20260429_relay_request_retries = + [r| +ALTER TABLE groups DROP COLUMN relay_request_retries; +ALTER TABLE groups DROP COLUMN relay_request_delay; +ALTER TABLE groups DROP COLUMN relay_request_execute_at; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260507_relay_inactive_at.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260507_relay_inactive_at.hs new file mode 100644 index 0000000000..f35927113c --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260507_relay_inactive_at.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260507_relay_inactive_at where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260507_relay_inactive_at :: Text +m20260507_relay_inactive_at = + [r| +ALTER TABLE groups ADD COLUMN relay_inactive_at TIMESTAMPTZ; +|] + +down_m20260507_relay_inactive_at :: Text +down_m20260507_relay_inactive_at = + [r| +ALTER TABLE groups DROP COLUMN relay_inactive_at; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260407_client_services.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260520_client_services.hs similarity index 57% rename from src/Simplex/Chat/Store/Postgres/Migrations/M20260407_client_services.hs rename to src/Simplex/Chat/Store/Postgres/Migrations/M20260520_client_services.hs index 581dab1285..af567130eb 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/M20260407_client_services.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260520_client_services.hs @@ -1,19 +1,19 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Store.Postgres.Migrations.M20260407_client_services where +module Simplex.Chat.Store.Postgres.Migrations.M20260520_client_services where import Data.Text (Text) import Text.RawString.QQ (r) -m20260407_client_services :: Text -m20260407_client_services = +m20260520_client_services :: Text +m20260520_client_services = [r| ALTER TABLE users ADD COLUMN client_service SMALLINT NOT NULL DEFAULT 0; |] -down_m20260407_client_services :: Text -down_m20260407_client_services = +down_m20260520_client_services :: Text +down_m20260520_client_services = [r| ALTER TABLE users DROP COLUMN client_service; |] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index fa368bf87f..cfa77e2b51 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -959,7 +959,11 @@ CREATE TABLE test_chat_schema.groups ( root_priv_key bytea, root_pub_key bytea, member_priv_key bytea, - public_member_count bigint + public_member_count bigint, + relay_request_retries bigint DEFAULT 0 NOT NULL, + relay_request_delay bigint DEFAULT 0 NOT NULL, + relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 04:00:00+04'::timestamp with time zone NOT NULL, + relay_inactive_at timestamp with time zone ); diff --git a/src/Simplex/Chat/Store/RelayRequests.hs b/src/Simplex/Chat/Store/RelayRequests.hs index 3858281878..2e590a1696 100644 --- a/src/Simplex/Chat/Store/RelayRequests.hs +++ b/src/Simplex/Chat/Store/RelayRequests.hs @@ -9,13 +9,15 @@ module Simplex.Chat.Store.RelayRequests ( hasPendingRelayRequests, getNextPendingRelayRequest, + updateRelayRequestRetries, setRelayRequestErr, ) where +import Data.Int (Int64) import Data.Maybe (fromMaybe) import Data.Text (Text) -import Data.Time.Clock (getCurrentTime) +import Data.Time.Clock (UTCTime, getCurrentTime) import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Shared @@ -64,7 +66,7 @@ getNextPendingRelayRequest db = WHERE relay_own_status = ? AND relay_request_failed = 0 AND relay_request_err_reason IS NULL - ORDER BY group_id ASC + ORDER BY relay_request_execute_at ASC LIMIT 1 |] (Only RSInvited) @@ -76,18 +78,27 @@ getNextPendingRelayRequest db = [sql| SELECT relay_request_inv_id, relay_request_group_link, - relay_request_peer_chat_min_version, relay_request_peer_chat_max_version + relay_request_peer_chat_min_version, relay_request_peer_chat_max_version, + relay_request_delay, relay_request_retries, created_at, relay_request_execute_at FROM groups WHERE group_id = ? |] (Only groupId) where - toRelayRequestData :: (Maybe InvitationId, Maybe ShortLinkContact, Maybe VersionChat, Maybe VersionChat) -> Either StoreError (GroupId, RelayRequestData) + toRelayRequestData :: (Maybe InvitationId, Maybe ShortLinkContact, Maybe VersionChat, Maybe VersionChat, Int64, Int, UTCTime, UTCTime) -> Either StoreError (GroupId, RelayRequestData) toRelayRequestData = \case - (Just relayInvId, Just reqGroupLink, Just minV, Just maxV) -> - Right (groupId, RelayRequestData {relayInvId, reqGroupLink, reqChatVRange = fromMaybe (versionToRange maxV) $ safeVersionRange minV maxV}) + (Just relayInvId, Just reqGroupLink, Just minV, Just maxV, reqDelay, reqRetries, reqCreatedAt, reqExecuteAt) -> + Right (groupId, RelayRequestData {relayInvId, reqGroupLink, reqChatVRange = fromMaybe (versionToRange maxV) $ safeVersionRange minV maxV, reqDelay, reqRetries, reqCreatedAt, reqExecuteAt}) _ -> Left $ SEInternalError "missing relay request data" +updateRelayRequestRetries :: DB.Connection -> GroupId -> Int64 -> UTCTime -> IO () +updateRelayRequestRetries db groupId delay executeAt = do + currentTs <- getCurrentTime + DB.execute + db + "UPDATE groups SET relay_request_retries = relay_request_retries + 1, relay_request_delay = ?, relay_request_execute_at = ?, updated_at = ? WHERE group_id = ?" + (delay, executeAt, currentTs, groupId) + markRelayRequestFailed :: DB.Connection -> GroupId -> IO () markRelayRequestFailed db groupId = do currentTs <- getCurrentTime diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index cdbdf1bc0a..dab6cf4d7d 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -151,7 +151,9 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260108_chat_indices import Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link import Simplex.Chat.Store.SQLite.Migrations.M20260222_chat_relays import Simplex.Chat.Store.SQLite.Migrations.M20260403_item_viewed -import Simplex.Chat.Store.SQLite.Migrations.M20260407_client_services +import Simplex.Chat.Store.SQLite.Migrations.M20260429_relay_request_retries +import Simplex.Chat.Store.SQLite.Migrations.M20260507_relay_inactive_at +import Simplex.Chat.Store.SQLite.Migrations.M20260520_client_services import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -303,7 +305,9 @@ schemaMigrations = ("20260122_has_link", m20260122_has_link, Just down_m20260122_has_link), ("20260222_chat_relays", m20260222_chat_relays, Just down_m20260222_chat_relays), ("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed), - ("20260407_client_services", m20260407_client_services, Just down_m20260407_client_services) + ("20260429_relay_request_retries", m20260429_relay_request_retries, Just down_m20260429_relay_request_retries), + ("20260507_relay_inactive_at", m20260507_relay_inactive_at, Just down_m20260507_relay_inactive_at), + ("20260520_client_services", m20260520_client_services, Just down_m20260520_client_services) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260429_relay_request_retries.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260429_relay_request_retries.hs new file mode 100644 index 0000000000..fea87f4baf --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260429_relay_request_retries.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260429_relay_request_retries where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260429_relay_request_retries :: Query +m20260429_relay_request_retries = + [sql| +ALTER TABLE groups ADD COLUMN relay_request_retries INTEGER NOT NULL DEFAULT 0; +ALTER TABLE groups ADD COLUMN relay_request_delay INTEGER NOT NULL DEFAULT 0; +ALTER TABLE groups ADD COLUMN relay_request_execute_at TEXT NOT NULL DEFAULT '1970-01-01 00:00:00'; +|] + +down_m20260429_relay_request_retries :: Query +down_m20260429_relay_request_retries = + [sql| +ALTER TABLE groups DROP COLUMN relay_request_retries; +ALTER TABLE groups DROP COLUMN relay_request_delay; +ALTER TABLE groups DROP COLUMN relay_request_execute_at; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260507_relay_inactive_at.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260507_relay_inactive_at.hs new file mode 100644 index 0000000000..0596d4892a --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260507_relay_inactive_at.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260507_relay_inactive_at where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260507_relay_inactive_at :: Query +m20260507_relay_inactive_at = + [sql| +ALTER TABLE groups ADD COLUMN relay_inactive_at TEXT; +|] + +down_m20260507_relay_inactive_at :: Query +down_m20260507_relay_inactive_at = + [sql| +ALTER TABLE groups DROP COLUMN relay_inactive_at; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260407_client_services.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260520_client_services.hs similarity index 56% rename from src/Simplex/Chat/Store/SQLite/Migrations/M20260407_client_services.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20260520_client_services.hs index ea160c9c4f..db141d6c03 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20260407_client_services.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260520_client_services.hs @@ -1,18 +1,18 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Store.SQLite.Migrations.M20260407_client_services where +module Simplex.Chat.Store.SQLite.Migrations.M20260520_client_services where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) -m20260407_client_services :: Query -m20260407_client_services = +m20260520_client_services :: Query +m20260520_client_services = [sql| ALTER TABLE users ADD COLUMN client_service INTEGER NOT NULL DEFAULT 0; |] -down_m20260407_client_services :: Query -down_m20260407_client_services = +down_m20260520_client_services :: Query +down_m20260520_client_services = [sql| ALTER TABLE users DROP COLUMN client_service; |] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index fdcac9134a..4becf5a2ee 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -273,6 +273,16 @@ Query: Plan: SEARCH connections USING PRIMARY KEY (conn_id=?) +Query: + SELECT user_id FROM users u + WHERE u.deleted = ? + AND NOT EXISTS (SELECT c.conn_id FROM connections c WHERE c.user_id = u.user_id) + +Plan: +SCAN u +CORRELATED SCALAR SUBQUERY 1 +SEARCH c USING COVERING INDEX idx_connections_user (user_id=?) + Query: SELECT user_id FROM users u WHERE u.user_id = ? @@ -555,6 +565,21 @@ Query: Plan: SEARCH conn_confirmations USING COVERING INDEX idx_conn_confirmations_conn_id (conn_id=?) +Query: + DELETE FROM encrypted_rcv_message_hashes + WHERE encrypted_rcv_message_hash_id IN ( + SELECT encrypted_rcv_message_hash_id + FROM encrypted_rcv_message_hashes + WHERE created_at < ? + ORDER BY created_at ASC + LIMIT ? + ) + +Plan: +SEARCH encrypted_rcv_message_hashes USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH encrypted_rcv_message_hashes USING COVERING INDEX idx_encrypted_rcv_message_hashes_created_at (created_at 0 Plan: SEARCH contacts USING INDEX idx_contacts_chat_ts (user_id=?) +Query: SELECT COUNT(1) FROM group_members WHERE member_role = 'owner' AND member_pub_key IS NOT NULL +Plan: +SCAN group_members + Query: SELECT COUNT(1) FROM groups WHERE user_id = ? AND chat_item_ttl > 0 Plan: SEARCH groups USING INDEX sqlite_autoindex_groups_2 (user_id=?) @@ -6756,6 +6816,10 @@ Query: SELECT last_insert_rowid() Plan: SCAN CONSTANT ROW +Query: SELECT local_display_name FROM group_members +Plan: +SCAN group_members USING COVERING INDEX idx_group_members_user_id_local_display_name + Query: SELECT max(active_order) FROM users Plan: SEARCH users @@ -7100,6 +7164,10 @@ Query: UPDATE groups SET relay_own_status = ?, updated_at = ? WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE groups SET relay_request_err_reason = ?, updated_at = ? WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET request_shared_msg_id = ? WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 801a1b9f0d..d2595f88ee 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -174,7 +174,11 @@ CREATE TABLE groups( root_priv_key BLOB, root_pub_key BLOB, member_priv_key BLOB, - public_member_count INTEGER, -- received + public_member_count INTEGER, + relay_request_retries INTEGER NOT NULL DEFAULT 0, + relay_request_delay INTEGER NOT NULL DEFAULT 0, + relay_request_execute_at TEXT NOT NULL DEFAULT '1970-01-01 00:00:00', + relay_inactive_at TEXT, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 8611fa5d73..1bd97e3029 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -52,7 +52,7 @@ import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import Simplex.FileTransfer.Description (FileDigest) import Simplex.FileTransfer.Types (RcvFileId, SndFileId) -import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink, ConnectionLink, ConnectionMode (..), ConnectionRequestUri, CreatedConnLink, InvitationId, SAEntity (..), UserId) +import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink (..), ConnectionLink, ConnectionMode (..), ConnectionRequestUri, ContactConnType (..), CreatedConnLink (..), InvitationId, SAEntity (..), UserId) import Simplex.Messaging.Agent.Store.DB (Binary (..), blobFieldDecoder, fromTextField_) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) @@ -496,9 +496,6 @@ data GroupInfo = GroupInfo useRelays' :: GroupInfo -> Bool useRelays' GroupInfo {useRelays} = isTrue useRelays -sendAsGroup' :: GroupInfo -> Bool -sendAsGroup' gInfo@GroupInfo {membership} = useRelays' gInfo && memberRole' membership == GROwner - groupId' :: GroupInfo -> GroupId groupId' GroupInfo {groupId} = groupId @@ -520,6 +517,18 @@ instance FromField BusinessChatType where fromField = fromTextField_ textDecode instance ToField BusinessChatType where toField = toField . textEncode +class HasShortLink l where + connShortLink' :: l c -> Maybe (ConnShortLink c) + +instance HasShortLink CreatedConnLink where + connShortLink' (CCLink _ sl) = sl + +setShortLinkType :: ContactConnType -> CreatedLinkContact -> CreatedLinkContact +setShortLinkType ct (CCLink cReq sl) = CCLink cReq (setShortLinkType_ ct <$> sl) + +setShortLinkType_ :: ContactConnType -> ShortLinkContact -> ShortLinkContact +setShortLinkType_ ct (CSLContact sch _ srv k) = CSLContact sch ct srv k + data PreparedGroup = PreparedGroup { connLinkToConnect :: CreatedLinkContact, connLinkPreparedConnection :: Bool, @@ -759,15 +768,18 @@ fromLocalProfile LocalProfile {displayName, fullName, shortDescr, image, contact data GroupType = GTChannel + | GTGroup | GTUnknown Text deriving (Eq, Show) instance TextEncoding GroupType where textEncode = \case GTChannel -> "channel" + GTGroup -> "group" GTUnknown tag -> tag textDecode s = Just $ case s of "channel" -> GTChannel + "group" -> GTGroup tag -> GTUnknown tag instance FromField GroupType where fromField = fromTextField_ textDecode @@ -1035,7 +1047,11 @@ data GroupMember = GroupMember data RelayRequestData = RelayRequestData { relayInvId :: InvitationId, reqGroupLink :: ShortLinkContact, - reqChatVRange :: VersionRangeChat + reqChatVRange :: VersionRangeChat, + reqDelay :: Int64, + reqRetries :: Int, + reqCreatedAt :: UTCTime, + reqExecuteAt :: UTCTime } deriving (Eq, Show) diff --git a/src/Simplex/Chat/Types/Preferences.hs b/src/Simplex/Chat/Types/Preferences.hs index d8c6f10b3a..be189379c9 100644 --- a/src/Simplex/Chat/Types/Preferences.hs +++ b/src/Simplex/Chat/Types/Preferences.hs @@ -176,7 +176,9 @@ data GroupFeature | GFSimplexLinks | GFReports | GFHistory + | GFSupport | GFSessions + | GFComments deriving (Show) data SGroupFeature (f :: GroupFeature) where @@ -189,7 +191,9 @@ data SGroupFeature (f :: GroupFeature) where SGFSimplexLinks :: SGroupFeature 'GFSimplexLinks SGFReports :: SGroupFeature 'GFReports SGFHistory :: SGroupFeature 'GFHistory + SGFSupport :: SGroupFeature 'GFSupport SGFSessions :: SGroupFeature 'GFSessions + SGFComments :: SGroupFeature 'GFComments deriving instance Show (SGroupFeature f) @@ -216,7 +220,9 @@ groupFeatureNameText = \case GFSimplexLinks -> "SimpleX links" GFReports -> "Member reports" GFHistory -> "Recent history" + GFSupport -> "Chat with admins" GFSessions -> "Chat sessions" + GFComments -> "Comments" groupFeatureNameText' :: SGroupFeature f -> Text groupFeatureNameText' = groupFeatureNameText . toGroupFeature @@ -230,6 +236,11 @@ groupFeatureMemberAllowed' feature role prefs = let pref = getGroupPreference feature prefs in getField @"enable" pref == FEOn && maybe True (role >=) (getField @"role" pref) +-- TODO: some preferences are channel-only (e.g., comments) and should not generate +-- UI items or be configurable in regular groups. Currently they are simply excluded +-- from this list. When more channel-only or group-only preferences are added, +-- consider adding a scope property to GroupFeatureI (e.g., GFScopeAll | GFScopeChannel | GFScopeGroup) +-- and filtering at the call sites in createGroupFeatureItems_ / createGroupFeatureChangedItems. allGroupFeatures :: [AGroupFeature] allGroupFeatures = [ AGF SGFTimedMessages, @@ -240,11 +251,12 @@ allGroupFeatures = AGF SGFFiles, AGF SGFSimplexLinks, AGF SGFReports, - AGF SGFHistory + AGF SGFHistory, + AGF SGFSupport ] groupPrefSel :: SGroupFeature f -> GroupPreferences -> Maybe (GroupFeaturePreference f) -groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, sessions} = case f of +groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, support, sessions, comments} = case f of SGFTimedMessages -> timedMessages SGFDirectMessages -> directMessages SGFFullDelete -> fullDelete @@ -254,7 +266,9 @@ groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reac SGFSimplexLinks -> simplexLinks SGFReports -> reports SGFHistory -> history + SGFSupport -> support SGFSessions -> sessions + SGFComments -> comments toGroupFeature :: SGroupFeature f -> GroupFeature toGroupFeature = \case @@ -267,7 +281,9 @@ toGroupFeature = \case SGFSimplexLinks -> GFSimplexLinks SGFReports -> GFReports SGFHistory -> GFHistory + SGFSupport -> GFSupport SGFSessions -> GFSessions + SGFComments -> GFComments class GroupPreferenceI p where getGroupPreference :: SGroupFeature f -> p -> GroupFeaturePreference f @@ -279,7 +295,7 @@ instance GroupPreferenceI (Maybe GroupPreferences) where getGroupPreference pt prefs = fromMaybe (getGroupPreference pt defaultGroupPrefs) (groupPrefSel pt =<< prefs) instance GroupPreferenceI FullGroupPreferences where - getGroupPreference f FullGroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, sessions} = case f of + getGroupPreference f FullGroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, support, sessions, comments} = case f of SGFTimedMessages -> timedMessages SGFDirectMessages -> directMessages SGFFullDelete -> fullDelete @@ -289,7 +305,9 @@ instance GroupPreferenceI FullGroupPreferences where SGFSimplexLinks -> simplexLinks SGFReports -> reports SGFHistory -> history + SGFSupport -> support SGFSessions -> sessions + SGFComments -> comments {-# INLINE getGroupPreference #-} -- collection of optional group preferences @@ -303,7 +321,9 @@ data GroupPreferences = GroupPreferences simplexLinks :: Maybe SimplexLinksGroupPreference, reports :: Maybe ReportsGroupPreference, history :: Maybe HistoryGroupPreference, + support :: Maybe SupportGroupPreference, sessions :: Maybe SessionsGroupPreference, + comments :: Maybe CommentsGroupPreference, commands :: Maybe [ChatBotCommand] } deriving (Eq, Show) @@ -353,7 +373,9 @@ setGroupPreference_ f pref prefs = SGFSimplexLinks -> prefs {simplexLinks = pref} SGFReports -> prefs {reports = pref} SGFHistory -> prefs {history = pref} + SGFSupport -> prefs {support = pref} SGFSessions -> prefs {sessions = pref} + SGFComments -> prefs {comments = pref} setGroupTimedMessagesPreference :: TimedMessagesGroupPreference -> Maybe GroupPreferences -> GroupPreferences setGroupTimedMessagesPreference pref prefs_ = @@ -395,7 +417,9 @@ data FullGroupPreferences = FullGroupPreferences simplexLinks :: SimplexLinksGroupPreference, reports :: ReportsGroupPreference, history :: HistoryGroupPreference, + support :: SupportGroupPreference, sessions :: SessionsGroupPreference, + comments :: CommentsGroupPreference, commands :: ListDef ChatBotCommand } deriving (Eq, Show) @@ -464,12 +488,14 @@ defaultGroupPrefs = simplexLinks = SimplexLinksGroupPreference {enable = FEOn, role = Nothing}, reports = ReportsGroupPreference {enable = FEOn}, history = HistoryGroupPreference {enable = FEOff}, + support = SupportGroupPreference {enable = FEOn}, sessions = SessionsGroupPreference {enable = FEOff, role = Nothing}, + comments = CommentsGroupPreference {enable = FEOff, duration = Nothing}, commands = ListDef [] } emptyGroupPrefs :: GroupPreferences -emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing +emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing businessGroupPrefs :: Preferences -> GroupPreferences businessGroupPrefs Preferences {timedMessages, fullDelete, reactions, voice, files, sessions, commands} = @@ -500,7 +526,9 @@ defaultBusinessGroupPrefs = simplexLinks = Just $ SimplexLinksGroupPreference FEOn Nothing, reports = Just $ ReportsGroupPreference FEOff, history = Just $ HistoryGroupPreference FEOn, + support = Just $ SupportGroupPreference FEOn, sessions = Just $ SessionsGroupPreference FEOn Nothing, + comments = Just $ CommentsGroupPreference FEOff Nothing, commands = Nothing } @@ -631,10 +659,22 @@ data HistoryGroupPreference = HistoryGroupPreference {enable :: GroupFeatureEnabled} deriving (Eq, Show) +data SupportGroupPreference = SupportGroupPreference + {enable :: GroupFeatureEnabled} + deriving (Eq, Show) + data SessionsGroupPreference = SessionsGroupPreference {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} deriving (Eq, Show) +-- Channel comments. ``duration` is time in seconds since post creation +-- after which a channel post stops accepting new comments; `Nothing` means accept comments indefinitely. +data CommentsGroupPreference = CommentsGroupPreference + { enable :: GroupFeatureEnabled, + duration :: Maybe Int + } + deriving (Eq, Show) + class (Eq (GroupFeaturePreference f), HasField "enable" (GroupFeaturePreference f) GroupFeatureEnabled) => GroupFeatureI f where type GroupFeaturePreference (f :: GroupFeature) = p | p -> f sGroupFeature :: SGroupFeature f @@ -675,9 +715,15 @@ instance HasField "enable" ReportsGroupPreference GroupFeatureEnabled where instance HasField "enable" HistoryGroupPreference GroupFeatureEnabled where hasField p@HistoryGroupPreference {enable} = (\e -> p {enable = e}, enable) +instance HasField "enable" SupportGroupPreference GroupFeatureEnabled where + hasField p@SupportGroupPreference {enable} = (\e -> p {enable = e}, enable) + instance HasField "enable" SessionsGroupPreference GroupFeatureEnabled where hasField p@SessionsGroupPreference {enable} = (\e -> p {enable = e}, enable) +instance HasField "enable" CommentsGroupPreference GroupFeatureEnabled where + hasField p@CommentsGroupPreference {enable} = (\e -> p {enable = e}, enable) + instance GroupFeatureI 'GFTimedMessages where type GroupFeaturePreference 'GFTimedMessages = TimedMessagesGroupPreference sGroupFeature = SGFTimedMessages @@ -732,12 +778,24 @@ instance GroupFeatureI 'GFHistory where groupPrefParam _ = Nothing groupPrefRole _ = Nothing +instance GroupFeatureI 'GFSupport where + type GroupFeaturePreference 'GFSupport = SupportGroupPreference + sGroupFeature = SGFSupport + groupPrefParam _ = Nothing + groupPrefRole _ = Nothing + instance GroupFeatureI 'GFSessions where type GroupFeaturePreference 'GFSessions = SessionsGroupPreference sGroupFeature = SGFSessions groupPrefParam _ = Nothing groupPrefRole SessionsGroupPreference {role} = role +instance GroupFeatureI 'GFComments where + type GroupFeaturePreference 'GFComments = CommentsGroupPreference + sGroupFeature = SGFComments + groupPrefParam CommentsGroupPreference {duration} = duration + groupPrefRole _ = Nothing + instance GroupFeatureNoRoleI 'GFTimedMessages instance GroupFeatureNoRoleI 'GFFullDelete @@ -748,6 +806,10 @@ instance GroupFeatureNoRoleI 'GFReports instance GroupFeatureNoRoleI 'GFHistory +instance GroupFeatureNoRoleI 'GFSupport + +instance GroupFeatureNoRoleI 'GFComments + instance HasField "role" DirectMessagesGroupPreference (Maybe GroupMemberRole) where hasField p@DirectMessagesGroupPreference {role} = (\r -> p {role = r}, role) @@ -788,6 +850,7 @@ groupPrefStateText feature pref param role = groupParamText_ :: GroupFeature -> Maybe Int -> Text groupParamText_ feature param = case feature of GFTimedMessages -> maybe "" (\p -> " (" <> timedTTLText p <> ")") param + GFComments -> maybe "" (\p -> " (close after " <> timedTTLText p <> ")") param _ -> "" groupPreferenceText :: forall f. GroupFeatureI f => GroupFeaturePreference f -> Text @@ -937,7 +1000,9 @@ mergeGroupPreferences groupPreferences = simplexLinks = pref SGFSimplexLinks, reports = pref SGFReports, history = pref SGFHistory, + support = pref SGFSupport, sessions = pref SGFSessions, + comments = pref SGFComments, commands = ListDef $ fromMaybe [] $ groupPreferences >>= commands_ } where @@ -956,7 +1021,9 @@ toGroupPreferences groupPreferences@FullGroupPreferences {commands = ListDef cmd simplexLinks = pref SGFSimplexLinks, reports = pref SGFReports, history = pref SGFHistory, + support = pref SGFSupport, sessions = pref SGFSessions, + comments = pref SGFComments, commands = Just cmds } where @@ -1085,11 +1152,19 @@ $(J.deriveJSON defaultJSON ''ReportsGroupPreference) $(J.deriveJSON defaultJSON ''HistoryGroupPreference) -$(J.deriveToJSON defaultJSON ''SessionsGroupPreference) +$(J.deriveToJSON defaultJSON ''SupportGroupPreference) -instance FromJSON SessionsGroupPreference where - parseJSON v = $(J.mkParseJSON defaultJSON ''SessionsGroupPreference) v - omittedField = Just SessionsGroupPreference {enable = FEOff, role = Nothing} +instance FromJSON SupportGroupPreference where + parseJSON v = $(J.mkParseJSON defaultJSON ''SupportGroupPreference) v + omittedField = Just SupportGroupPreference {enable = FEOn} + +$(J.deriveJSON defaultJSON ''SessionsGroupPreference) + +$(J.deriveToJSON defaultJSON ''CommentsGroupPreference) + +instance FromJSON CommentsGroupPreference where + parseJSON v = $(J.mkParseJSON defaultJSON ''CommentsGroupPreference) v + omittedField = Just CommentsGroupPreference {enable = FEOff, duration = Nothing} $(J.deriveJSON defaultJSON ''GroupPreferences) diff --git a/src/Simplex/Chat/Types/Shared.hs b/src/Simplex/Chat/Types/Shared.hs index 22cb73f325..e0630e2e42 100644 --- a/src/Simplex/Chat/Types/Shared.hs +++ b/src/Simplex/Chat/Types/Shared.hs @@ -83,6 +83,7 @@ data RelayStatus | RSInvited | RSAccepted | RSActive + | RSInactive deriving (Eq, Show) relayStatusText :: RelayStatus -> Text @@ -91,6 +92,7 @@ relayStatusText = \case RSInvited -> "invited" RSAccepted -> "accepted" RSActive -> "active" + RSInactive -> "inactive" instance TextEncoding RelayStatus where textEncode = \case @@ -98,11 +100,13 @@ instance TextEncoding RelayStatus where RSInvited -> "invited" RSAccepted -> "accepted" RSActive -> "active" + RSInactive -> "inactive" textDecode = \case "new" -> Just RSNew "invited" -> Just RSInvited "accepted" -> Just RSAccepted "active" -> Just RSActive + "inactive" -> Just RSInactive _ -> Nothing instance FromField RelayStatus where fromField = fromTextField_ textDecode diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index f522c27a1a..c786ac8b53 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -22,7 +22,7 @@ import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isSpace, toUpper) import Data.Function (on) import Data.Int (Int64) -import Data.List (groupBy, intercalate, intersperse, sortOn) +import Data.List (groupBy, intercalate, intersperse, nub, sortOn) import Data.List.NonEmpty (NonEmpty (..)) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M @@ -148,8 +148,8 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code] CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView - CRNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz - CRChatItems u _ chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts tz <> viewItemReactions item) chatItems + CRNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz testView + CRChatItems u _ chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts tz <> viewItemReactions item <> viewTestInfo testView item) chatItems CRChatItemInfo u ci ciInfo -> ttyUser u $ viewChatItemInfo ci ciInfo tz CRChatItemId u itemId -> ttyUser u [plain $ maybe "no item" show itemId] CRChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewItemUpdate chat item liveItems ts tz @@ -180,7 +180,10 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRContactRequestRejected u UserContactRequest {localDisplayName = c} _ct_ -> ttyUser u [ttyContact c <> ": contact request rejected"] CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView CRPublicGroupCreated u g _groupLink _relays -> ttyUser u $ viewGroupCreated g testView + CRPublicGroupCreationFailed u results -> ttyUser u $ viewPublicGroupCreationFailed results CRGroupRelays u g relays -> ttyUser u $ viewGroupRelays g relays + CRGroupRelaysAdded u g _groupLink relays -> ttyUser u $ viewGroupRelays g relays + CRGroupRelaysAddFailed u results -> ttyUser u $ viewGroupRelaysAddFailed results CRGroupMembers u g -> ttyUser u $ viewGroupMembers g CRMemberSupportChats u g ms -> ttyUser u $ viewMemberSupportChats g ms -- CRGroupConversationsArchived u _g _conversations -> ttyUser u [] @@ -222,6 +225,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRLeftMemberUser u g -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g CRGroupDeletedUser u g signed -> ttyUser u [ttyGroup' g <> ": you deleted the group" <> signedStr signed] CRForwardPlan u count itemIds fc -> ttyUser u $ viewForwardPlan count itemIds fc + CRChatMsgContent u mc -> ttyUser u $ ttyMsgContent mc <> viewMsgTestInfo testView mc CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft CRSndFileCancelled u _ ftm fts -> ttyUser u $ viewSndFileCancelled ftm fts @@ -407,7 +411,7 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} CEvtContactRatchetSync u ct progress -> ttyUser u $ viewContactRatchetSync ct progress CEvtGroupMemberRatchetSync u g m progress -> ttyUser u $ viewGroupMemberRatchetSync g m progress CEvtChatInfoUpdated _ _ -> [] - CEvtNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz + CEvtNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz testView CEvtChatItemsStatusesUpdated u chatItems | length chatItems <= 20 -> concatMap @@ -648,11 +652,12 @@ viewChatItems :: [AChatItem] -> UTCTime -> TimeZone -> + Bool -> [StyledString] -viewChatItems ttyUser unmuted u chatItems ts tz +viewChatItems ttyUser unmuted u chatItems ts tz testView | length chatItems <= 20 = concatMap - (\(AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item) + (\(AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item <> viewTestInfo testView item) chatItems | all (\aci -> aChatItemDir aci == MDRcv) chatItems = ttyUser u [sShow (length chatItems) <> " new messages"] | all (\aci -> aChatItemDir aci == MDSnd) chatItems = ttyUser u [sShow (length chatItems) <> " messages sent"] @@ -673,6 +678,7 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa CIDirectRcv -> case content of CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta + CIRcvMsgError err -> viewRcvMsgError from err ts tz meta CIRcvGroupEvent {} -> showRcvItemProhibited from _ -> showRcvItem from where @@ -696,6 +702,7 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa rcvGroupItem m_ = case content of CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta + CIRcvMsgError err -> viewRcvMsgError from err ts tz meta CIRcvGroupInvitation {} | isJust m_ -> showRcvItemProhibited from CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g scopeInfo m_) context meta [plainContent content] False CIRcvBlocked {} -> receivedWithTime_ ts tz (ttyFromGroup g scopeInfo m_) context meta [plainContent content] False @@ -717,6 +724,7 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa CILocalRcv -> case content of CIRcvMsgContent mc -> withLocalFile from $ rcvMsg from context mc CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta + CIRcvMsgError err -> viewRcvMsgError from err ts tz meta CIRcvGroupEvent {} -> showRcvItemProhibited from _ -> showRcvItem from where @@ -948,6 +956,14 @@ viewItemReactions ChatItem {reactions} = [" " <> viewReactions reactions | viewReaction CIReactionCount {reaction = MREmoji (MREmojiChar emoji), userReacted, totalReacted} = plain [emoji, ' '] <> (if userReacted then styled Italic else plain) (show totalReacted) +viewTestInfo :: Bool -> ChatItem c d -> [StyledString] +viewTestInfo testView ChatItem {content} = maybe [] (viewMsgTestInfo testView) $ ciMsgContent content + +viewMsgTestInfo :: Bool -> MsgContent -> [StyledString] +viewMsgTestInfo testView = \case + MCChat {ownerSig = Just sig} | testView -> [viewJSON sig] + _ -> [] + viewReactionMembers :: [MemberReaction] -> [StyledString] viewReactionMembers memberReactions = [sShow (length memberReactions) <> " member(s) reacted"] @@ -993,6 +1009,9 @@ viewRcvIntegrityError from msgErr ts tz meta = receivedWithTime_ ts tz from [] m viewMsgIntegrityError :: MsgErrorType -> [StyledString] viewMsgIntegrityError err = [ttyError $ msgIntegrityError err] +viewRcvMsgError :: StyledString -> RcvMsgError -> CurrentTime -> TimeZone -> CIMeta c 'MDRcv -> [StyledString] +viewRcvMsgError from rcvErr ts tz meta = receivedWithTime_ ts tz from [] meta [ttyError $ rcvMsgErrorText rcvErr] False + viewInvalidConnReq :: [StyledString] viewInvalidConnReq = [ "", @@ -1224,6 +1243,18 @@ viewGroupCreated g testView = where relaysInstruction = "wait for selected relay(s) to join, then you can invite members via group link" +viewRelayResults :: StyledString -> [AddRelayResult] -> [StyledString] +viewRelayResults header results = [header] <> map showRelayResult results + where + showRelayResult (AddRelayResult UserChatRelay {chatRelayId = DBEntityId i} err_) = + " relay " <> sShow i <> ": " <> maybe "ok" (plain . tshow) err_ + +viewPublicGroupCreationFailed :: [AddRelayResult] -> [StyledString] +viewPublicGroupCreationFailed = viewRelayResults "channel not created, results:" + +viewGroupRelaysAddFailed :: [AddRelayResult] -> [StyledString] +viewGroupRelaysAddFailed = viewRelayResults "relays not added, results:" + viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString] viewCannotResendInvitation g c = [ ttyContact c <> " is already invited to group " <> ttyGroup' g, @@ -2045,7 +2076,7 @@ viewGroupUserChanged viewConnectionPlan :: ChatConfig -> ACreatedConnLink -> ConnectionPlan -> [StyledString] viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case CPInvitationLink ilp -> case ilp of - ILPOk contactSLinkData -> [invOrBiz contactSLinkData "ok to connect"] <> [viewJSON contactSLinkData | testView] + ILPOk contactSLinkData ov -> [invOrBiz contactSLinkData "ok to connect"] <> viewSigVerification ov <> [viewJSON contactSLinkData | testView] ILPOwnLink -> [invLink "own link"] ILPConnecting Nothing -> [invLink "connecting"] ILPConnecting (Just ct) -> [invLink ("connecting to contact " <> ttyContact' ct)] @@ -2063,7 +2094,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case | business -> ("business address: " <>) _ -> ("invitation link: " <>) CPContactAddress cap -> case cap of - CAPOk contactSLinkData -> [addrOrBiz contactSLinkData "ok to connect"] <> [viewJSON contactSLinkData | testView] + CAPOk contactSLinkData ov -> [addrOrBiz contactSLinkData "ok to connect"] <> viewSigVerification ov <> [viewJSON contactSLinkData | testView] CAPOwnLink -> [ctAddr "own address"] CAPConnectingConfirmReconnect -> [ctAddr "connecting, allowed to reconnect"] CAPConnectingProhibit ct -> [ctAddr ("connecting to contact " <> ttyContact' ct)] @@ -2081,15 +2112,16 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case | business -> ("business address: " <>) _ -> ("contact address: " <>) CPGroupLink glp -> case glp of - GLPOk groupSLinkInfo_ groupSLinkData -> + GLPOk groupSLinkInfo_ groupSLinkData ov -> let direct = maybe True (\(GroupShortLinkInfo {direct = d}) -> d) groupSLinkInfo_ in [grpLink $ if direct then "ok to connect directly" else "ok to connect via relays"] + <> viewSigVerification ov <> [viewJSON groupSLinkData | testView] GLPOwnLink g -> [grpLink "own link for group " <> ttyGroup' g] GLPConnectingConfirmReconnect -> [grpLink "connecting, allowed to reconnect"] GLPConnectingProhibit Nothing -> [grpLink "connecting"] GLPConnectingProhibit (Just g) -> connecting g - GLPKnown g@GroupInfo {preparedGroup, membership = m} -> case preparedGroup of + GLPKnown g@GroupInfo {preparedGroup, membership = m} _ _ _ -> case preparedGroup of Just PreparedGroup {connLinkStartedConnection} -> case memberStatus m of GSMemUnknown | connLinkStartedConnection -> connecting g @@ -2105,6 +2137,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case "use " <> ttyToGroup g Nothing <> highlight' "" <> " to send messages" ] knownGroup prepared = grpOrBizLink g <> ": known " <> prepared <> grpOrBiz g <> " " <> ttyGroup' g + GLPNoRelays _ -> [grpLink "channel has no active relays, please try to join later"] where connecting g = [grpOrBizLink g <> ": connecting to " <> grpOrBiz g <> " " <> ttyGroup' g] grpLink = ("group link: " <>) @@ -2119,6 +2152,10 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case nextConnectPrepared Contact {preparedContact, activeConn} = case preparedContact of Just _ -> maybe True (\c -> connStatus c == ConnPrepared) activeConn _ -> False + viewSigVerification = \case + Just OVVerified -> ["owner signature: verified"] + Just (OVFailed r) -> ["owner signature: FAILED (" <> plain r <> ")"] + Nothing -> [] viewContactUpdated :: Contact -> Contact -> [StyledString] viewContactUpdated @@ -2218,7 +2255,26 @@ sentWithTime_ ts tz styledMsg CIMeta {itemTs} = prependFirst (ttyMsgTime ts tz itemTs <> " ") styledMsg ttyMsgContent :: MsgContent -> [StyledString] -ttyMsgContent = msgPlain . msgContentText +ttyMsgContent = \case + MCChat {text, chatLink, ownerSig} -> + let (linkInfo, name, links) = viewChatLink chatLink + signed = if isJust ownerSig then " (signed)" else "" + body = if T.null text || text `elem` links then [] else msgPlain text + in [plain $ linkInfo <> viewName name <> signed <> ":"] <> map plain links <> body + mc -> msgPlain $ msgContentText mc + where + viewChatLink = \case + MCLGroup {connLink, groupProfile = GroupProfile {displayName, publicGroup}} -> + let (ref, links) = case publicGroup of + Just PublicGroupProfile {groupType, groupLink} -> (textEncode groupType, nub [enc connLink, enc groupLink]) + Nothing -> ("group", [enc connLink]) + in ("link to join " <> ref <> " #", displayName, links) + MCLContact {connLink, profile = Profile {displayName}} -> + ("contact address of @", displayName, [enc connLink]) + MCLInvitation {invLink, profile = Profile {displayName}} -> + ("one-time link of @", displayName, [enc invLink]) + enc :: StrEncoding a => a -> Text + enc = safeDecodeUtf8 . strEncode prependFirst :: StyledString -> [StyledString] -> [StyledString] prependFirst s [] = [s] @@ -2667,7 +2723,7 @@ viewChatError isCmd logLevel testView = \case BRContent -> "content violates conditions of use" BROKER _ (NETWORK _) | not isCmd -> [] BROKER _ TIMEOUT | not isCmd -> [] - AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug || isCmd] + AGENT A_DUPLICATE {} -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug || isCmd] AGENT (A_PROHIBITED e) -> [withConnEntity <> "error: AGENT A_PROHIBITED, " <> plain e | logLevel <= CLLWarning || isCmd] CONN NOT_FOUND _ -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning || isCmd] CRITICAL restart e -> [plain $ "critical error: " <> e] <> ["please restart the app" | restart] diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index f765f2bbc0..828c44e9a4 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -7,7 +7,9 @@ module Bots.DirectoryTests where import ChatClient +import ChatTests.ChatRelays (withRelay) import ChatTests.DBUtils +import ChatTests.Groups (memberJoinChannel, prepareChannel1Relay) import ChatTests.Utils import Control.Concurrent (forkIO, killThread, threadDelay) import Control.Exception (finally) @@ -42,6 +44,7 @@ directoryServiceTests = do it "should support group names with spaces" testGroupNameWithSpaces it "should return more groups in search, all and recent groups" testSearchGroups it "should invite to owners' group if specified" testInviteToOwnersGroup + it "should re-invite owner who left owners' group" testInviteOwnerAfterLeavingOwnersGroup 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 @@ -85,6 +88,13 @@ directoryServiceTests = do describe "help commands" $ do it "should not list audio command" testHelpNoAudio it "should reject audio command in DM" testAudioCommandInDM + describe "public group registration" $ do + it "should register channel via shared link card" testRegisterChannelViaCard + it "should suggest share via chat when link sent as text" testLinkAsTextSearch + it "should reject card shared by non-owner" testNonOwnerSharesCard + it "should delete channel registration and leave" testDeleteChannelRegistration + it "should handle re-registration when already listed" testReregistrationAlreadyListed + it "should update subscriber count periodically" testLinkCheckUpdatesCount directoryProfile :: Profile directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing} @@ -121,6 +131,7 @@ mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup webFolder = runCLI = False, searchResults = 3, webFolder, + linkCheckInterval = 0, testing = True } @@ -568,6 +579,32 @@ testInviteToOwnersGroup ps = registerGroupId superUser bob "security" "Security" 3 2 superUser <## "Owner is already a member of owners' group" +testInviteOwnerAfterLeavingOwnersGroup :: HasCallStack => TestParams -> IO () +testInviteOwnerAfterLeavingOwnersGroup ps = + withDirectoryServiceCfgOwnersGroup ps testCfg True Nothing $ \superUser dsLink -> + withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> do + bob `connectVia` dsLink + registerGroupId superUser bob "privacy" "Privacy" 2 1 + bob <## "#owners: 'SimpleX Directory' invites you to join the group as member" + bob <## "use /j owners to accept" + superUser <## "Invited @bob, the owner of the group ID 2 (privacy) to owners' group owners" + bob ##> "/j owners" + bob <## "#owners: you joined the group" + bob <## "#owners: member alice (Alice) is connected" + superUser <## "#owners: 'SimpleX Directory' added bob (Bob) to the group (connecting...)" + superUser <## "#owners: new member bob is connected" + -- owner leaves owners' group; GroupMember row keeps status GSMemLeft + leaveGroup "owners" bob + superUser <## "#owners: bob left the group" + -- owners' group has no GroupReg, so directory service notifies admins on contact left + superUser <# "'SimpleX Directory'> Error: contact left, group: 1 owners, group registration not found" + -- super-user re-invites via /invite — must send a fresh invitation, not "already a member" + superUser #> "@'SimpleX Directory' /invite 2:privacy" + superUser <# "'SimpleX Directory'> > /invite 2:privacy" + superUser <## " you invited @bob, the owner of the group ID 2 (privacy) to owners' group owners" + bob <## "#owners_1: 'SimpleX Directory' invites you to join the group as member" + bob <## "use /j owners_1 to accept" + testDelistedOwnerLeaves :: HasCallStack => TestParams -> IO () testDelistedOwnerLeaves ps = withDirectoryService ps $ \superUser dsLink -> @@ -1772,7 +1809,7 @@ u `connectVia` dsLink = do u .<# "> Welcome to SimpleX Directory!" u <## "" u <## "🔍 Send search string to find groups - try security." - u <## "/help - how to submit your group." + u <## "/help - how to submit your group or channel." u <## "/new - recent groups." u <## "" u <## "[Directory rules](https://simplex.chat/docs/directory.html)." @@ -1923,7 +1960,7 @@ testHelpNoAudio ps = -- commands help should not mention /audio bob #> "@'SimpleX Directory' /help commands" bob <# "'SimpleX Directory'> /'help commands' - receive this help message." - bob <## "/help - how to register your group to be added to directory." + bob <## "/help - how to register your group or channel to be added to directory." bob <## "/list - list the groups you registered." bob <## "`/role ` - view and set default member role for your group." bob <## "`/filter ` - view and set spam filter settings for group." @@ -1941,6 +1978,266 @@ testAudioCommandInDM ps = bob <# "'SimpleX Directory'> > /audio" bob <## " Unknown command" +testRegisterChannelViaCard :: HasCallStack => TestParams -> IO () +testRegisterChannelViaCard ps = + withDirectoryServiceCfg ps testCfg $ \superUser dsLink -> + withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> + withRelay ps $ \relay -> do + -- bob connects to directory service first + bob `connectVia` dsLink + -- bob creates a channel with a relay + (_shortLink, _fullLink) <- prepareChannel1Relay "news" bob relay + -- bob shares the channel card with directory bot + bob ##> "/share chat #news @'SimpleX Directory'" + bob <# "@'SimpleX Directory' link to join channel #news (signed):" + _ <- getTermLine bob -- short link + _ <- getTermLine bob -- ownerSig JSON + -- directory bot validates and joins via relay + bob <# "'SimpleX Directory'> Joining the channel news…" + concurrentlyN_ + [ do + relay <## "'SimpleX Directory': accepting request to join group #news..." + relay <## "#news: 'SimpleX Directory' joined the group", + bob <## "#news: relay added 'SimpleX Directory_1' to the group" + ] + -- owner sends a message to trigger member introduction + bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours." + superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:" + superUser <## "news" + superUser <##. "Link to join channel: " + superUser <## "You need SimpleX Chat app v6.5 to join." + superUser <## "1 subscribers" + superUser <## "" + superUser <## "To approve send:" + superUser <# "'SimpleX Directory'> /approve 1:news 1" + -- superuser approves + let approve = "/approve 1:news 1" + superUser #> ("@'SimpleX Directory' " <> approve) + superUser <# ("'SimpleX Directory'> > " <> approve) + superUser <## " Channel approved!" + bob <# ("'SimpleX Directory'> The channel ID 1 (news) is approved and listed in directory - please moderate it!") + bob <## "Please note: if you change the channel profile it will be hidden from directory until it is re-approved." + -- owner updates channel profile, triggering re-approval + bob ##> "/gp news news News and Updates" + bob <## "description changed to: News and Updates" + bob <# "'SimpleX Directory'> The channel ID 1 (news) is updated." + bob <## "It is hidden from the directory until approved." + relay <## "bob updated group #news: (signed)" + relay <## "description changed to: News and Updates" + superUser <# "'SimpleX Directory'> The channel ID 1 (news) is updated." + superUser <# ("'SimpleX Directory'> bob submitted the channel ID 1:") + superUser <## "news (News and Updates)" + superUser <##. "Link to join channel: " + superUser <## "You need SimpleX Chat app v6.5 to join." + superUser <## "2 subscribers" + superUser <## "" + superUser <## "To approve send:" + superUser <# "'SimpleX Directory'> /approve 1:news 1" + -- re-approve after profile update + let approve2 = "/approve 1:news 1" + superUser #> ("@'SimpleX Directory' " <> approve2) + superUser <# ("'SimpleX Directory'> > " <> approve2) + superUser <## " Channel approved!" + bob <# ("'SimpleX Directory'> The channel ID 1 (news) is approved and listed in directory - please moderate it!") + bob <## "Please note: if you change the channel profile it will be hidden from directory until it is re-approved." + -- owner leaves channel, triggering de-listing and bot leaving + bob ##> "/leave #news" + concurrentlyN_ + [ do + bob <## "#news: you left the group" + bob <## "use /d #news to delete the group", + relay <## "#news: bob left the group (signed)" + ] + bob <# "'SimpleX Directory'> You left the channel ID 1 (news)." + bob <## "" + bob <## "The channel is no longer listed in the directory." + superUser <# "'SimpleX Directory'> The channel ID 1 (news) is de-listed (channel owner left)." + relay <## "#news: 'SimpleX Directory' left the group (signed)" + +testLinkAsTextSearch :: HasCallStack => TestParams -> IO () +testLinkAsTextSearch ps = + withDirectoryServiceCfg ps testCfg $ \_superUser dsLink -> + withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> + withRelay ps $ \relay -> do + bob `connectVia` dsLink + (shortLink, _fullLink) <- prepareChannel1Relay "news" bob relay + bob #> ("@'SimpleX Directory' " <> shortLink) + bob <# ("'SimpleX Directory'> > " <> shortLink) + bob <## " No groups found." + bob <## "To register a group or a channel, please use \"Share via chat\" feature." + +testNonOwnerSharesCard :: HasCallStack => TestParams -> IO () +testNonOwnerSharesCard ps = + withDirectoryServiceCfg ps testCfg $ \_superUser dsLink -> + withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> + withRelay ps $ \relay -> + withNewTestChatCfg ps testCfg "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + cath `connectVia` dsLink + (shortLink, fullLink) <- prepareChannel1Relay "news" bob relay + memberJoinChannel "news" [relay] [bob] shortLink fullLink cath + cath ##> "/share chat #news @'SimpleX Directory'" + cath <# "@'SimpleX Directory' link to join channel #news:" + _ <- getTermLine cath -- short link + cath <# "'SimpleX Directory'> To add a channel to directory you must be the owner." + +testDeleteChannelRegistration :: HasCallStack => TestParams -> IO () +testDeleteChannelRegistration ps = + withDirectoryServiceCfg ps testCfg $ \superUser dsLink -> + withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> + withRelay ps $ \relay -> do + bob `connectVia` dsLink + (_shortLink, _fullLink) <- prepareChannel1Relay "news" bob relay + bob ##> "/share chat #news @'SimpleX Directory'" + bob <# "@'SimpleX Directory' link to join channel #news (signed):" + _ <- getTermLine bob -- short link + _ <- getTermLine bob -- ownerSig JSON + bob <# "'SimpleX Directory'> Joining the channel news…" + concurrentlyN_ + [ do + relay <## "'SimpleX Directory': accepting request to join group #news..." + relay <## "#news: 'SimpleX Directory' joined the group", + bob <## "#news: relay added 'SimpleX Directory_1' to the group" + ] + bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours." + superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:" + superUser <## "news" + superUser <##. "Link to join channel: " + superUser <## "You need SimpleX Chat app v6.5 to join." + superUser <## "1 subscribers" + superUser <## "" + superUser <## "To approve send:" + superUser <# "'SimpleX Directory'> /approve 1:news 1" + let approve = "/approve 1:news 1" + superUser #> ("@'SimpleX Directory' " <> approve) + superUser <# ("'SimpleX Directory'> > " <> approve) + superUser <## " Channel approved!" + bob <# ("'SimpleX Directory'> The channel ID 1 (news) is approved and listed in directory - please moderate it!") + bob <## "Please note: if you change the channel profile it will be hidden from directory until it is re-approved." + -- owner deletes registration + bob #> "@'SimpleX Directory' /delete 1:news" + bob + <### + [ WithTime "'SimpleX Directory'> > /delete 1:news", + " Your channel news is deleted from the directory", + "#news: 'SimpleX Directory_1' left the group (signed)" + ] + relay <## "#news: 'SimpleX Directory' left the group (signed)" + +testReregistrationAlreadyListed :: HasCallStack => TestParams -> IO () +testReregistrationAlreadyListed ps = + withDirectoryServiceCfg ps testCfg $ \superUser dsLink -> + withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> + withRelay ps $ \relay -> do + bob `connectVia` dsLink + (_shortLink, _fullLink) <- prepareChannel1Relay "news" bob relay + -- register and approve + bob ##> "/share chat #news @'SimpleX Directory'" + bob <# "@'SimpleX Directory' link to join channel #news (signed):" + _ <- getTermLine bob -- short link + _ <- getTermLine bob -- ownerSig JSON + bob <# "'SimpleX Directory'> Joining the channel news…" + concurrentlyN_ + [ do + relay <## "'SimpleX Directory': accepting request to join group #news..." + relay <## "#news: 'SimpleX Directory' joined the group", + bob <## "#news: relay added 'SimpleX Directory_1' to the group" + ] + bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours." + superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:" + superUser <## "news" + superUser <##. "Link to join channel: " + superUser <## "You need SimpleX Chat app v6.5 to join." + superUser <## "1 subscribers" + superUser <## "" + superUser <## "To approve send:" + superUser <# "'SimpleX Directory'> /approve 1:news 1" + let approve = "/approve 1:news 1" + superUser #> ("@'SimpleX Directory' " <> approve) + superUser <# ("'SimpleX Directory'> > " <> approve) + superUser <## " Channel approved!" + bob <# ("'SimpleX Directory'> The channel ID 1 (news) is approved and listed in directory - please moderate it!") + bob <## "Please note: if you change the channel profile it will be hidden from directory until it is re-approved." + -- search finds the channel with its link + bob #> "@'SimpleX Directory' news" + bob <# "'SimpleX Directory'> > news" + bob <## " Found 1 group(s)." + bob <# "'SimpleX Directory'> news" + bob <##. "Link to join channel: " + bob <## "You need SimpleX Chat app v6.5 to join." + bob <## "1 subscribers" + -- owner re-shares card while already listed + bob ##> "/share chat #news @'SimpleX Directory'" + bob <# "@'SimpleX Directory' link to join channel #news (signed):" + _ <- getTermLine bob -- short link + _ <- getTermLine bob -- ownerSig JSON + bob <# "'SimpleX Directory'> Channel is already listed in the directory." + +testLinkCheckUpdatesCount :: HasCallStack => TestParams -> IO () +testLinkCheckUpdatesCount ps = do + dsLink <- + withNewTestChatCfg ps testCfg serviceDbPrefix directoryProfile $ \ds -> + withNewTestChatCfg ps testCfg "super_user" aliceProfile $ \superUser -> do + connectUsers ds superUser + ds ##> "/ad" + getContactLink ds True + let opts = (mkDirectoryOpts ps [KnownContact 2 "alice"] Nothing Nothing) {linkCheckInterval = 1} + runDirectory testCfg opts $ + withTestChatCfg ps testCfg "super_user" $ \superUser -> do + superUser <## "subscribed 1 connections on server localhost" + withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> + withRelay ps $ \relay -> + withNewTestChatCfg ps testCfg "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + (shortLink, fullLink) <- prepareChannel1Relay "news" bob relay + -- register and approve + bob ##> "/share chat #news @'SimpleX Directory'" + bob <# "@'SimpleX Directory' link to join channel #news (signed):" + _ <- getTermLine bob -- short link + _ <- getTermLine bob -- ownerSig JSON + bob <# "'SimpleX Directory'> Joining the channel news…" + concurrentlyN_ + [ do + relay <## "'SimpleX Directory': accepting request to join group #news..." + relay <## "#news: 'SimpleX Directory' joined the group", + bob <## "#news: relay added 'SimpleX Directory_1' to the group" + ] + bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours." + superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:" + superUser <## "news" + superUser <##. "Link to join channel: " + superUser <## "You need SimpleX Chat app v6.5 to join." + superUser <## "1 subscribers" + superUser <## "" + superUser <## "To approve send:" + superUser <# "'SimpleX Directory'> /approve 1:news 1" + let approve = "/approve 1:news 1" + superUser #> ("@'SimpleX Directory' " <> approve) + superUser <# ("'SimpleX Directory'> > " <> approve) + superUser <## " Channel approved!" + bob <# ("'SimpleX Directory'> The channel ID 1 (news) is approved and listed in directory - please moderate it!") + bob <## "Please note: if you change the channel profile it will be hidden from directory until it is re-approved." + -- link check updates count (bot joined) + threadDelay 1000000 + bob #> "@'SimpleX Directory' news" + bob <# "'SimpleX Directory'> > news" + bob <## " Found 1 group(s)." + bob <# "'SimpleX Directory'> news" + bob <##. "Link to join channel: " + bob <## "You need SimpleX Chat app v6.5 to join." + bob <## "2 subscribers" + -- second subscriber joins + memberJoinChannel "news" [relay] [bob] shortLink fullLink cath + -- link check updates count again + threadDelay 1000000 + bob #> "@'SimpleX Directory' news" + bob <# "'SimpleX Directory'> > news" + bob <## " Found 1 group(s)." + bob <# "'SimpleX Directory'> news" + bob <##. "Link to join channel: " + bob <## "You need SimpleX Chat app v6.5 to join." + bob <## "3 subscribers" + testGetCaptchaStr :: HasCallStack => TestParams -> IO () testGetCaptchaStr _ps = do s0 <- getCaptchaStr 0 "" diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index cab69fe10e..ede3c1f2a2 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -42,7 +42,8 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Shared (GroupMemberRole (..)) import Simplex.FileTransfer.Description (kb, mb) import Simplex.FileTransfer.Server (runXFTPServerBlocking) -import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration) +import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), XFTPStoreConfig (..), defaultFileExpiration) +import Simplex.FileTransfer.Server.Store import Simplex.FileTransfer.Transport (alpnSupportedXFTPhandshakes, supportedFileServerVRange) import Simplex.Messaging.Agent (disposeAgentClient) import Simplex.Messaging.Agent.Env.SQLite @@ -170,7 +171,7 @@ termSettings :: VirtualTerminalSettings termSettings = VirtualTerminalSettings { virtualType = "xterm", - virtualWindowSize = pure C.Size {height = 20, width = 6000}, + virtualWindowSize = pure C.Size {height = 24, width = 6000}, virtualEvent = retry, virtualInterrupt = retry } @@ -599,11 +600,12 @@ xftpTestPort = "7002" xftpServerFiles :: FilePath xftpServerFiles = "tests/tmp/xftp-server-files" -xftpServerConfig :: XFTPServerConfig +xftpServerConfig :: XFTPServerConfig STMFileStore xftpServerConfig = XFTPServerConfig { xftpPort = xftpTestPort, fileIdSize = 16, + serverStoreCfg = XSCMemory $ Just "tests/tmp/xftp-server-store.log", storeLogFile = Just "tests/tmp/xftp-server-store.log", filesPath = xftpServerFiles, fileSizeQuota = Nothing, @@ -638,7 +640,7 @@ xftpServerConfig = withXFTPServer :: IO () -> IO () withXFTPServer = withXFTPServer' xftpServerConfig -withXFTPServer' :: XFTPServerConfig -> IO () -> IO () +withXFTPServer' :: XFTPServerConfig STMFileStore -> IO () -> IO () withXFTPServer' cfg = serverBracket ( \started -> do diff --git a/tests/ChatTests/ChatList.hs b/tests/ChatTests/ChatList.hs index 14d48dbf60..889915a6e8 100644 --- a/tests/ChatTests/ChatList.hs +++ b/tests/ChatTests/ChatList.hs @@ -200,14 +200,14 @@ testPaginationAllChatTypes = ts7 <- iso8601Show <$> getCurrentTime - getChats_ alice "count=10" [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("@cath", "Audio/video calls: enabled"), ("@bob", "hey")] - getChats_ alice "count=3" [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on")] + getChats_ alice "count=10" [("*", "psst"), ("@dan", "hey"), ("#team", "Chat with admins: on"), (":3", ""), ("@cath", "Audio/video calls: enabled"), ("@bob", "hey")] + getChats_ alice "count=3" [("*", "psst"), ("@dan", "hey"), ("#team", "Chat with admins: on")] getChats_ alice ("after=" <> ts2 <> " count=2") [(":3", ""), ("@cath", "Audio/video calls: enabled")] - getChats_ alice ("before=" <> ts5 <> " count=2") [("#team", "Recent history: on"), (":3", "")] - getChats_ alice ("after=" <> ts3 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", "")] + getChats_ alice ("before=" <> ts5 <> " count=2") [("#team", "Chat with admins: on"), (":3", "")] + getChats_ alice ("after=" <> ts3 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Chat with admins: on"), (":3", "")] getChats_ alice ("before=" <> ts4 <> " count=10") [(":3", ""), ("@cath", "Audio/video calls: enabled"), ("@bob", "hey")] - getChats_ alice ("after=" <> ts1 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("@cath", "Audio/video calls: enabled"), ("@bob", "hey")] - getChats_ alice ("before=" <> ts7 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("@cath", "Audio/video calls: enabled"), ("@bob", "hey")] + getChats_ alice ("after=" <> ts1 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Chat with admins: on"), (":3", ""), ("@cath", "Audio/video calls: enabled"), ("@bob", "hey")] + getChats_ alice ("before=" <> ts7 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Chat with admins: on"), (":3", ""), ("@cath", "Audio/video calls: enabled"), ("@bob", "hey")] getChats_ alice ("after=" <> ts7 <> " count=10") [] getChats_ alice ("before=" <> ts1 <> " count=10") [] @@ -219,11 +219,11 @@ testPaginationAllChatTypes = alice ##> "/_settings #1 {\"enableNtfs\":\"all\",\"favorite\":true}" alice <## "ok" - getChats_ alice queryFavorite [("#team", "Recent history: on"), ("@bob", "hey")] + getChats_ alice queryFavorite [("#team", "Chat with admins: on"), ("@bob", "hey")] getChats_ alice ("before=" <> ts4 <> " count=1 " <> queryFavorite) [("@bob", "hey")] - getChats_ alice ("before=" <> ts5 <> " count=1 " <> queryFavorite) [("#team", "Recent history: on")] + getChats_ alice ("before=" <> ts5 <> " count=1 " <> queryFavorite) [("#team", "Chat with admins: on")] getChats_ alice ("after=" <> ts1 <> " count=1 " <> queryFavorite) [("@bob", "hey")] - getChats_ alice ("after=" <> ts4 <> " count=1 " <> queryFavorite) [("#team", "Recent history: on")] + getChats_ alice ("after=" <> ts4 <> " count=1 " <> queryFavorite) [("#team", "Chat with admins: on")] let queryUnread = "{\"type\": \"filters\", \"favorite\": false, \"unread\": true}" diff --git a/tests/ChatTests/ChatRelays.hs b/tests/ChatTests/ChatRelays.hs index 721d71d0e0..58fe1074ef 100644 --- a/tests/ChatTests/ChatRelays.hs +++ b/tests/ChatTests/ChatRelays.hs @@ -1,9 +1,23 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedStrings #-} + module ChatTests.ChatRelays where import ChatClient import ChatTests.DBUtils +import ChatTests.Groups (memberJoinChannel, memberJoinChannel', prepareChannel, prepareChannel', prepareChannel1Relay, setupRelay) import ChatTests.Utils import Control.Concurrent (threadDelay) +import qualified Data.Aeson as J +import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Maybe (fromMaybe) +import qualified Data.Text as T +import ProtocolTests (testGroupProfile) +import Simplex.Chat.Protocol (LinkOwnerSig, MsgChatLink (..), MsgContent (..)) +import Simplex.Chat.Types (GroupProfile (..)) +import Simplex.Messaging.Encoding.String (StrEncoding (..)) +import Simplex.Messaging.Util (decodeJSON) import Test.Hspec hiding (it) chatRelayTests :: SpecWith TestParams @@ -14,6 +28,10 @@ chatRelayTests = do it "re-add soft-deleted relay by same name" testReAddRelaySameName it "test chat relay" testChatRelayTest it "relay profile updated in address" testRelayProfileUpdateInAddress + describe "share channel card" $ do + it "share channel card in direct chat" testShareChannelDirect + it "share channel card in group" testShareChannelGroup + it "share channel card in channel" testShareChannelChannel testGetSetChatRelays :: HasCallStack => TestParams -> IO () testGetSetChatRelays ps = @@ -165,6 +183,151 @@ testRelayProfileUpdateInAddress ps = alice ##> ("/relay test " <> bobSLink) alice <## "relay test passed, profile: bob2 (Bob relay)" +testShareChannelDirect :: HasCallStack => TestParams -> IO () +testShareChannelDirect ps = + testChat3 aliceProfile bobProfile cathProfile test ps + where + test alice bob cath = withRelay ps $ \relay -> do + (shortLink, fullLink) <- prepareChannel1Relay "news" alice relay + connectUsers alice bob + -- alice gets ownerSig from share content API (for validation later) + alice ##> "/_share chat content #1 @2" + alice <## "link to join channel #news (signed):" + (_, apiOwnerSig) <- getTermLine2 alice + -- alice sends the card to bob + alice ##> "/share chat #news @bob" + alice <# "@bob link to join channel #news (signed):" + _ <- getTermLine2 alice -- alice's testView ownerSig + bob <# "alice> link to join channel #news (signed):" + -- bob captures the received ownerSig from message view (testView) + (sLink, cSig) <- getTermLine2 bob + sLink `shouldBe` shortLink + cSig `shouldBe` apiOwnerSig + -- bob verifies owner signature via connect plan + bob ##> ("/_connect plan 1 " <> shortLink <> " sig=" <> cSig) + bob <## "group link: ok to connect via relays" + bob <## "owner signature: verified" + _ <- getTermLine bob -- group link data + -- bob joins + memberJoinChannel' "news" 1 0 1 0 [relay] [alice] shortLink fullLink bob + connectUsers bob cath + -- bob (subscriber) shares unsigned - not owner + bob ##> "/share chat #news @cath" + bob <# "@cath link to join channel #news:" + _ <- getTermLine bob + cath <# "bob> link to join channel #news:" + _ <- getTermLine cath + -- bob tries to replay alice's signed card to cath - binding mismatch, sig stripped at receive + let sig = fromMaybe (error "bad sig") (decodeJSON (T.pack cSig) :: Maybe LinkOwnerSig) + cLink = either error id $ strDecode (B.pack sLink) + mc = MCChat (T.pack sLink) (MCLGroup cLink (testGroupProfile {displayName = "news"} :: GroupProfile)) (Just sig) + cm = "{\"msgContent\":" <> LB.unpack (J.encode mc) <> "}" + bob ##> ("/_send @3 json [" <> cm <> "]") + bob <# "@cath link to join group #news (signed):" + _ <- getTermLine2 bob -- bob's testView ownerSig (his sent has the sig data) + -- cath sees it without signature - binding was for alice->bob, not bob->cath, sig stripped + cath <# "bob> link to join group #news:" + _ <- getTermLine cath + -- cath joins anyway + memberJoinChannel "news" [relay] [alice] shortLink fullLink cath + alice #> "#news hello" + relay <# "#news> hello" + [bob, cath] *<# "#news> hello [>>]" + +testShareChannelGroup :: HasCallStack => TestParams -> IO () +testShareChannelGroup ps = + testChat3 aliceProfile bobProfile cathProfile test ps + where + test alice bob cath = withRelay ps $ \relay -> do + (shortLink, fullLink) <- prepareChannel1Relay "news" alice relay + createGroup2 "team" alice bob + alice ##> "/share chat #news #team" + alice <# "#team link to join channel #news:" + _ <- getTermLine alice + bob <# "#team alice> link to join channel #news:" + sLink <- getTermLine bob + sLink `shouldBe` shortLink + memberJoinChannel' "news" 2 0 1 0 [relay] [alice] sLink fullLink bob + createGroup2 "work" bob cath + bob ##> "/share chat #news #work" + bob <# "#work link to join channel #news:" + _ <- getTermLine bob + cath <# "#work bob> link to join channel #news:" + _ <- getTermLine cath + memberJoinChannel' "news" 2 0 0 0 [relay] [alice] shortLink fullLink cath + alice #> "#news hello" + relay <# "#news> hello" + [bob, cath] *<# "#news> hello [>>]" + +testShareChannelChannel :: HasCallStack => TestParams -> IO () +testShareChannelChannel ps = + testChat3 aliceProfile bobProfile cathProfile test ps + where + test alice bob cath = withRelay ps $ \relay -> do + relaySLink <- setupRelay alice relay + (sLink1, fLink1) <- prepareChannel "news" alice relay + (sLink2, fLink2) <- prepareChannel' 2 "updates" alice relay + -- bob joins "updates" first (relay doesn't know bob yet, no suffix) + memberJoinChannel "updates" [relay] [alice] sLink2 fLink2 bob + -- alice (owner) shares "news" to "updates" - signed + alice ##> "/_share chat content #1 #2(as_group=on)" + alice <## "link to join channel #news (signed):" + (apiLink, apiOwnerSig) <- getTermLine2 alice + apiLink `shouldBe` sLink1 + alice ##> "/share chat #news #updates" + alice <# "#updates link to join channel #news (signed):" + _ <- getTermLine2 alice -- link, ownerSig + relay <# "#updates> link to join channel #news (signed):" + _ <- getTermLine2 relay -- link, ownerSig + bob <# "#updates> link to join channel #news (signed): [>>]" + (cLink, cSig) <- getTermLine2 bob + cLink `shouldBe` (sLink1 <> " [>>]") + cSig `shouldBe` apiOwnerSig + -- bob verifies alice's signature via connect plan + bob ##> ("/_connect plan 1 " <> sLink1 <> " sig=" <> cSig) + bob <## "group link: ok to connect via relays" + bob <## "owner signature: verified" + _ <- getTermLine bob -- group link data + -- bob joins "news" (group #2 for bob, relay knows bob from "updates" so sfx=1) + memberJoinChannel' "news" 2 1 1 1 [relay] [alice] sLink1 fLink1 bob + -- bob creates channel "bob_ch" for delivery to cath + bob ##> ("/relays name=relay " <> relaySLink) + bob <## "ok" + (sLink3, fLink3) <- prepareChannel "bob_ch" bob relay + memberJoinChannel "bob_ch" [relay] [bob] sLink3 fLink3 cath + -- bob (subscriber) shares "news" to "bob_ch" - unsigned (not owner) + bob ##> "/share chat #news #bob_ch" + bob <# "#bob_ch link to join channel #news:" + _ <- getTermLine bob + relay <# "#bob_ch> link to join channel #news:" + _ <- getTermLine relay + cath <# "#bob_ch> link to join channel #news: [>>]" + _ <- getTermLine cath + -- bob tries to replay alice's signed card to bob_ch - binding mismatch, sig stripped at receive + let sig = fromMaybe (error "bad sig") (decodeJSON (T.pack cSig) :: Maybe LinkOwnerSig) + cLink' = either error id $ strDecode (B.pack sLink1) + mc = MCChat (T.pack sLink1) (MCLGroup cLink' (testGroupProfile {displayName = "news"} :: GroupProfile)) (Just sig) + cm = "{\"msgContent\":" <> LB.unpack (J.encode mc) <> "}" + bob ##> ("/_send #3 json [" <> cm <> "]") + bob <# "#bob_ch link to join group #news (signed):" + _ <- getTermLine2 bob -- bob's testView ownerSig (his sent has the sig data) + relay <# "#bob_ch bob_2> link to join group #news:" + _ <- getTermLine relay + cath <# "#bob_ch bob> link to join group #news: [>>]" + _ <- getTermLine cath + -- cath joins "news" (group #2 for cath since "bob_ch" is #1) + memberJoinChannel' "news" 2 1 0 1 [relay] [alice] sLink1 fLink1 cath + -- alice sends message, both receive + alice #> "#news hello" + relay <# "#news> hello" + [bob, cath] *<# "#news> hello [>>]" + +getTermLine2 :: TestCC -> IO (String, String) +getTermLine2 c = (,) <$> getTermLine c <*> getTermLine c + +withRelay :: HasCallStack => TestParams -> (TestCC -> IO ()) -> IO () +withRelay ps = withNewTestChatOpts ps relayTestOpts "relay" relayProfile + -- Create a public group with relay=1, wait for relay to join createChannelWithRelay :: HasCallStack => String -> TestCC -> TestCC -> IO () createChannelWithRelay gName owner relay = do diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 880c1373e9..fb94194561 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -19,7 +19,7 @@ import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Library.Internal (roundedFDCount) import Simplex.Chat.Mobile.File import Simplex.Chat.Options (ChatOpts (..)) -import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..)) +import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), XFTPStoreConfig (..)) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Encoding.String import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist, getFileSize) @@ -940,7 +940,7 @@ testXFTPRcvError ps = do alice <## "completed uploading file 1 (test.pdf) for bob" -- server is up w/t store log - file reception should fail - withXFTPServer' xftpServerConfig {storeLogFile = Nothing} $ do + withXFTPServer' xftpServerConfig {serverStoreCfg = XSCMemory Nothing, storeLogFile = Nothing} $ do withTestChat ps "bob" $ \bob -> do bob <## "subscribed 1 connections on server localhost" bob ##> "/fr 1 ./tests/tmp" diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 3a75a00a20..c9e60cd66e 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -28,6 +28,7 @@ import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHook import Simplex.Chat.Library.Internal (uniqueMsgMentions, updatedMentionNames) import Simplex.Chat.Markdown (parseMaybeMarkdownList) import Simplex.Chat.Messages (CIMention (..), CIMentionMember (..), ChatItemId) +import Simplex.Chat.Messages.CIContent (publicGroupNoE2EText) import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgMention (..), MsgContent (..), msgContentText) import Simplex.Chat.Types @@ -230,9 +231,10 @@ chatGroupTests = do it "should remove support chat with member when member is removed" testScopedSupportMemberRemoved it "should remove support chat with member when user removes member" testScopedSupportUserRemovesMember it "should remove support chat with member when member leaves" testScopedSupportMemberLeaves + it "should respect support preference in group" testSupportPreferenceGroup + it "should respect support preference in channel" testSupportPreferenceChannel -- TODO [relays] add tests for channels -- TODO - tests with delivery loop over members restored after restart - -- TODO - delivery in support scopes inside channels -- TODO - connect plans for relay groups -- TODO - cancellation on failure to create relay group (for owner) -- TODO - async retry connecting to relay (for members) @@ -251,6 +253,9 @@ chatGroupTests = do it "should share same incognito profile with all relays" testChannels2RelaysIncognito describe "channel operations" $ do it "should update channel profile (signed)" testChannelUpdateProfileSigned + it "should preserve working link after profile update" testChannelLinkAfterProfileUpdate + it "should preserve working link after welcome message update" testChannelLinkAfterWelcomeUpdate + it "should preserve owner key in link data after profile update" testChannelOwnerKeyAfterLinkUpdate it "should update channel preferences (signed)" testChannelUpdatePrefsSigned it "should change member role (signed)" testChannelChangeRoleSigned it "should block member for all (signed)" testChannelBlockMemberSigned @@ -259,8 +264,14 @@ chatGroupTests = do it "should delete channel and clean up relay connections" testChannelDeleteGroupCleanup it "owner should leave channel (signed)" testChannelOwnerLeave it "subscriber should leave channel (signed)" testChannelSubscriberLeave + it "relay should leave channel" testChannelRelayLeave it "owner should update profile in channel (signed)" testChannelOwnerProfileUpdate it "subscriber should update profile in channel (signed)" testChannelSubscriberProfileUpdate + it "should report relay results when one relay deleted its address" testChannelCreateDeletedRelay + it "should deliver support scope messages via relay" testChannelSupportScope + it "should add relay to existing channel" testChannelAddRelay + it "should remove relay from channel" testChannelRemoveRelay + it "should remove left relay from channel" testChannelRemoveLeftRelay describe "channel message operations" $ do it "should update channel message" testChannelMessageUpdate it "should delete channel message" testChannelMessageDelete @@ -451,7 +462,7 @@ testChatPaginationInitial = testChatOpts2 opts aliceProfile bobProfile $ \alice forM_ ([1 .. 10] :: [Int]) $ \n -> bob <# ("#team alice> " <> show n) -- All messages are unread for bob, should return area around unread - bob #$> ("/_get chat #1 initial=2", chat, [(0, "Recent history: on"), (0, "connected"), (0, "1"), (0, "2"), (0, "3")]) + bob #$> ("/_get chat #1 initial=2", chat, [(0, "Chat with admins: on"), (0, "connected"), (0, "1"), (0, "2"), (0, "3")]) -- Read next 2 items let itemIds = intercalate "," $ map groupItemId [1 .. 2] @@ -640,7 +651,7 @@ testGroup2 = ] dan <##> alice -- show last messages - alice ##> "/t #club 20" + alice ##> "/t #club 21" alice -- these strings are expected in any order because of sorting by time and rounding of time for sent <##? ( map (ConsoleString . ("#club " <> )) groupFeatureStrs @@ -1661,6 +1672,7 @@ testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile alice <## "SimpleX links: on" alice <## "Member reports: on" alice <## "Recent history: on" + alice <## "Chat with admins: on" bobAddedDan :: HasCallStack => TestCC -> IO () bobAddedDan cc = do cc <## "#team: bob added dan (Daniel) to the group (connecting...)" @@ -8394,6 +8406,120 @@ testScopedSupportMemberLeaves = { markRead = False } +testSupportPreferenceGroup :: HasCallStack => TestParams -> IO () +testSupportPreferenceGroup = + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + createGroup3' "team" alice (bob, GRMember) (cath, GRMember) + + threadDelay 1000000 + + -- support enabled by default, bob sends to support + bob #> "#team (support) hello" + alice <# "#team (support: bob) bob> hello" + + -- alice replies + alice #> "#team (support: bob) hi" + bob <# "#team (support) alice> hi" + + -- alice disables support + alice ##> "/set support #team off" + alice <## "updated group preferences:" + alice <## "Chat with admins: off" + concurrentlyN_ + [ do + bob <## "alice updated group #team:" + bob <## "updated group preferences:" + bob <## "Chat with admins: off", + do + cath <## "alice updated group #team:" + cath <## "updated group preferences:" + cath <## "Chat with admins: off" + ] + + threadDelay 500000 + + -- cath can't send support (no existing chat) + cath ##> "#team (support) hey" + cath <## "bad chat command: feature not allowed Chat with admins" + + -- alice can't send to cath's support (no existing chat) + alice ##> "#team (support: cath) hey" + alice <## "bad chat command: feature not allowed Chat with admins" + + -- bob can still send (existing chat) + bob #> "#team (support) still here" + alice <# "#team (support: bob) bob> still here" + + -- alice can still send to bob (existing chat) + alice #> "#team (support: bob) yes" + bob <# "#team (support) alice> yes" + +testSupportPreferenceChannel :: HasCallStack => TestParams -> IO () +testSupportPreferenceChannel ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "relay" relayProfile $ \relay -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice relay + memberJoinChannel "team" [relay] [alice] shortLink fullLink bob + memberJoinChannel "team" [relay] [alice] shortLink fullLink cath + + threadDelay 1000000 + + alice ##> "/set support #team on" + alice <## "updated group preferences:" + alice <## "Chat with admins: on" + toggledSupport relay "alice" "team" "on" + concurrentlyN_ + [ toggledSupport bob "alice" "team" "on", + toggledSupport cath "alice" "team" "on" + ] + + -- support enabled by default, bob sends to support + bob #> "#team (support) hello" + relay <# "#team (support: bob) bob> hello" + alice <# "#team (support: bob) bob> hello [>>]" + + -- alice replies + alice #> "#team (support: bob) hi" + relay <# "#team (support: bob) alice> hi" + bob <# "#team (support) alice> hi [>>]" + + -- alice disables support + + threadDelay 1000000 + + alice ##> "/set support #team off" + alice <## "updated group preferences:" + alice <## "Chat with admins: off" + toggledSupport relay "alice" "team" "off" + concurrentlyN_ + [ toggledSupport bob "alice" "team" "off", + toggledSupport cath "alice" "team" "off" + ] + + threadDelay 500000 + + -- cath can't send support (no existing chat) + cath ##> "#team (support) hey" + cath <## "bad chat command: feature not allowed Chat with admins" + alice ##> "#team (support: cath) hey too" + alice <## "bad chat command: feature not allowed Chat with admins" + + -- bob can still send (existing chat) + bob #> "#team (support) still here" + concurrentlyN_ + [ relay <# "#team (support: bob) bob> still here", + alice <# "#team (support: bob) bob> still here [>>]" + ] + + -- alice can still send to bob (existing chat) + alice #> "#team (support: bob) yes" + concurrentlyN_ + [ relay <# "#team (support: bob) alice> yes", + bob <# "#team (support) alice> yes [>>]" + ] + testChannels1RelayDeliver :: HasCallStack => TestParams -> IO () testChannels1RelayDeliver ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do @@ -8437,16 +8563,25 @@ createChannel1Relay gName owner relay cath dan eve = do forM_ [cath, dan, eve] $ \member -> memberJoinChannel gName [relay] [owner] shortLink fullLink member -prepareChannel1Relay :: String -> TestCC -> TestCC -> IO (String, String) -prepareChannel1Relay gName owner relay = do +setupRelay :: TestCC -> TestCC -> IO String +setupRelay owner relay = do rName <- userName relay - relay ##> "/ad" (relaySLink, _cLink) <- getContactLinks relay True - owner ##> ("/relays name=" <> rName <> " " <> relaySLink) owner <## "ok" + pure relaySLink +prepareChannel1Relay :: String -> TestCC -> TestCC -> IO (String, String) +prepareChannel1Relay gName owner relay = do + _ <- setupRelay owner relay + prepareChannel gName owner relay + +prepareChannel :: String -> TestCC -> TestCC -> IO (String, String) +prepareChannel = prepareChannel' 1 + +prepareChannel' :: Int -> String -> TestCC -> TestCC -> IO (String, String) +prepareChannel' relayId gName owner relay = do owner ##> ("/public group relays=1 #" <> gName) owner <## ("group #" <> gName <> " is created") owner <## "wait for selected relay(s) to join, then you can invite members via group link" @@ -8454,7 +8589,7 @@ prepareChannel1Relay gName owner relay = do concurrentlyN_ [ do owner <## ("#" <> gName <> ": group link relays updated, current relays:") - owner <## " - relay id 1: active" + owner <## (" - relay id " <> show relayId <> ": active") owner <## "group link:" _ <- getTermLine owner pure (), @@ -8513,10 +8648,17 @@ prepareChannel2Relays gName owner relay1 relay2 = do getGroupLinks owner gName GRMember False memberJoinChannel :: String -> [TestCC] -> [TestCC] -> String -> String -> TestCC -> IO () -memberJoinChannel gName relays owners shortLink fullLink member = do +memberJoinChannel gName = memberJoinChannel' gName 1 0 0 0 + +-- | sfx params: relaySfx - how relay/owner see the member, memberRelaySfx - how member sees relay +memberJoinChannel' :: String -> Int -> Int -> Int -> Int -> [TestCC] -> [TestCC] -> String -> String -> TestCC -> IO () +memberJoinChannel' gName gId relaySfx ownerSfx memberRelaySfx relays owners shortLink fullLink member = do mName <- userName member mFullName <- showName member - relayNames <- mapM userName relays + let sfxMName s = if s == 0 then mName else mName <> "_" <> show s + sfxName s = if s == 0 then mFullName else sfxMName s <> drop (length mName) mFullName + sfxRelayName rn = if memberRelaySfx == 0 then rn else rn <> "_" <> show memberRelaySfx + relayNames <- mapM (\r -> sfxRelayName <$> userName r) relays member ##> ("/_connect plan 1 " <> shortLink) member <## "group link: ok to connect via relays" @@ -8525,7 +8667,7 @@ memberJoinChannel gName relays owners shortLink fullLink member = do member ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " direct=off " <> groupSLinkData) member <## ("#" <> gName <> ": group is prepared") - member ##> "/_connect group #1" + member ##> ("/_connect group #" <> show gId) member <## ("#" <> gName <> ": connection started") concurrentlyN_ $ [ member @@ -8537,11 +8679,11 @@ memberJoinChannel gName relays owners shortLink fullLink member = do ] ] <> [ do - relay <## (mFullName <> ": accepting request to join group #team...") - relay <## ("#" <> gName <> ": " <> mName <> " joined the group") + relay <## (sfxName relaySfx <> ": accepting request to join group #" <> gName <> "...") + relay <## ("#" <> gName <> ": " <> sfxMName relaySfx <> " joined the group") | relay <- relays ] - <> [ owner <### [EndsWith ("added " <> mFullName <> " to the group")] + <> [ owner <### [EndsWith ("added " <> sfxName ownerSfx <> " to the group")] | owner <- owners ] @@ -8569,7 +8711,7 @@ memberJoinChannelIncognito gName relays owners shortLink fullLink member = do ] ] <> [ do - relay <## (memIncognito <> ": accepting request to join group #team...") + relay <## (memIncognito <> ": accepting request to join group #" <> gName <> "...") relay <## ("#" <> gName <> ": " <> memIncognito <> " joined the group") | relay <- relays ] @@ -8770,6 +8912,132 @@ testChannelUpdateProfileSigned ps = ] alice #$> ("/_get chat #1 count=1", chat, [(1, "group profile updated (signed)")]) +testChannelLinkAfterProfileUpdate :: HasCallStack => TestParams -> IO () +testChannelLinkAfterProfileUpdate ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + + -- owner updates channel profile + alice ##> "/gp team my_team My team description" + alice <## "changed to #my_team (My team description)" + concurrentlyN_ + [ do + bob <## "alice updated group #team: (signed)" + bob <## "changed to #my_team (My team description)", + do + cath <## "alice updated group #team: (signed)" + cath <## "changed to #my_team (My team description)" + ] + alice #$> ("/_get chat #1 count=1", chat, [(1, "group profile updated (signed)")]) + + -- late subscriber joins via the same channel link after profile update + threadDelay 100000 + alice ##> "/show link #my_team" + (shortLink', fullLink') <- getGroupLinks alice "my_team" GRMember False + shortLink' `shouldBe` shortLink + fullLink' `shouldBe` fullLink + memberJoinChannel "my_team" [bob] [alice] shortLink' fullLink' dan + + alice #> "#my_team hi" + bob <# "#my_team> hi" + [cath, dan] *<# "#my_team> hi [>>]" + +testChannelLinkAfterWelcomeUpdate :: HasCallStack => TestParams -> IO () +testChannelLinkAfterWelcomeUpdate ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + + -- owner updates channel welcome message + alice ##> "/set welcome #team welcome to team" + alice <## "welcome message changed to:" + alice <## "welcome to team" + concurrentlyN_ + [ do + bob <## "alice updated group #team: (signed)" + bob <## "welcome message changed to:" + bob <## "welcome to team", + do + cath <## "alice updated group #team: (signed)" + cath <## "welcome message changed to:" + cath <## "welcome to team" + ] + alice #$> ("/_get chat #1 count=1", chat, [(1, "group profile updated (signed)")]) + + -- re-fetch updated link, late subscriber joins + threadDelay 100000 + alice ##> "/show link #team" + (shortLink', fullLink') <- getGroupLinks alice "team" GRMember False + shortLink' `shouldBe` shortLink + fullLink' `shouldBe` fullLink + memberJoinChannel "team" [bob] [alice] shortLink' fullLink' dan + dan #$> ("/_get chat #1 count=100", chat, channelFeaturesNoE2E <> [(0, "welcome to team"), (0, T.unpack publicGroupNoE2EText), (0, "connected")]) + + alice #> "#team hi" + bob <# "#team> hi" + [cath, dan] *<# "#team> hi [>>]" + +testChannelOwnerKeyAfterLinkUpdate :: HasCallStack => TestParams -> IO () +testChannelOwnerKeyAfterLinkUpdate ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + + threadDelay 100000 + + -- Owner updates channel profile - triggers rebuilding link data. + alice ##> "/gp team my_team My team description" + alice <## "changed to #my_team (My team description)" + concurrentlyN_ + [ do + bob <## "alice updated group #team: (signed)" + bob <## "changed to #my_team (My team description)", + do + cath <## "alice updated group #team: (signed)" + cath <## "changed to #my_team (My team description)" + ] + + threadDelay 100000 + + -- Late subscriber joins via the same channel link after profile update. + alice ##> "/show link #my_team" + (shortLink', fullLink') <- getGroupLinks alice "my_team" GRMember False + shortLink' `shouldBe` shortLink + fullLink' `shouldBe` fullLink + memberJoinChannel "my_team" [bob] [alice] shortLink' fullLink' dan + + threadDelay 100000 + + -- Verify owner member record in late subscriber's DB has a public key. + ownerKeyPresent <- withCCTransaction dan $ \db -> + DB.query_ db "SELECT COUNT(1) FROM group_members WHERE member_role = 'owner' AND member_pub_key IS NOT NULL" :: IO [[Int]] + ownerKeyPresent `shouldBe` [[1]] + + -- Verify signed event is received by late subscriber. + alice ##> "/gp my_team team" + alice <## "changed to #team" + concurrentlyN_ + [ do + bob <## "alice updated group #my_team: (signed)" + bob <## "changed to #team", + do + cath <## "alice updated group #my_team: (signed)" + cath <## "changed to #team", + do + dan <## "alice updated group #my_team: (signed)" + dan <## "changed to #team" + ] + testChannelUpdatePrefsSigned :: HasCallStack => TestParams -> IO () testChannelUpdatePrefsSigned ps = withNewTestChat ps "alice" aliceProfile $ \alice -> @@ -9185,6 +9453,78 @@ testChannelSubscriberLeave ps = DB.query db "SELECT member_status FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only T.Text] map (\(Only s) -> s) statuses `shouldBe` maybeToList expected +testChannelRelayLeave :: HasCallStack => TestParams -> IO () +testChannelRelayLeave ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel2Relays "team" alice bob cath + forM_ [dan, eve] $ \member -> + memberJoinChannel "team" [bob, cath] [alice] shortLink fullLink member + + -- verify channel works + alice #> "#team hello" + [bob, cath] *<# "#team> hello" + [dan, eve] *<# "#team> hello [>>]" + + -- relay1 (bob) leaves + threadDelay 100000 + bob ##> "/leave #team" + bob <## "#team: you left the group" + bob <## "use /d #team to delete the group" + concurrentlyN_ + [ alice <## "#team: bob left the group (signed)", + -- cath: not notified (relays not connected, owner doesn't forward) + dan <## "#team: bob left the group (signed)", + eve <## "#team: bob left the group (signed)" + ] + + -- verify relay1 member status is "left" on all clients that know bob + checkMemberStatus alice "bob" (Just "left") + checkMemberStatus dan "bob" (Just "left") + checkMemberStatus eve "bob" (Just "left") + + -- verify channel still works with remaining relay + threadDelay 100000 + alice #> "#team still working" + cath <# "#team> still working" + [dan, eve] *<# "#team> still working [>>]" + + -- relay2 (cath) leaves + threadDelay 100000 + cath ##> "/leave #team" + cath <## "#team: you left the group" + cath <## "use /d #team to delete the group" + concurrentlyN_ + [ alice <## "#team: cath left the group (signed)", + dan <## "#team: cath left the group (signed)", + eve <## "#team: cath left the group (signed)" + ] + + -- verify relay2 member status + checkMemberStatus alice "cath" (Just "left") + checkMemberStatus dan "cath" (Just "left") + checkMemberStatus eve "cath" (Just "left") + + -- verify no delivery: owner sends but no relays to forward + alice #> "#team no delivery" + (dan ("/_connect plan 1 " <> shortLink) + frank <## "group link: channel has no active relays, please try to join later" + where + checkMemberStatus :: HasCallStack => TestCC -> T.Text -> Maybe T.Text -> IO () + checkMemberStatus cc name expected = do + statuses <- withCCTransaction cc $ \db -> + DB.query db "SELECT member_status FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only T.Text] + map (\(Only s) -> s) statuses `shouldBe` maybeToList expected + testChannelOwnerProfileUpdate :: HasCallStack => TestParams -> IO () testChannelOwnerProfileUpdate ps = withNewTestChat ps "alice" aliceProfile $ \alice -> @@ -9253,6 +9593,25 @@ testChannelSubscriberProfileUpdate ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- enable support and create support chat for cath (but not dan) + threadDelay 1000000 + alice ##> "/set support #team on" + alice <## "updated group preferences:" + alice <## "Chat with admins: on" + toggledSupport bob "alice" "team" "on" + concurrentlyN_ + [ toggledSupport cath "alice" "team" "on", + toggledSupport dan "alice" "team" "on", + toggledSupport eve "alice" "team" "on" + ] + + threadDelay 1000000 + alice #> "#team (support: cath) welcome" + bob <# "#team (support: cath) alice> welcome" + cath <# "#team (support) alice> welcome [>>]" + (dan "#team hello from cath" @@ -9268,6 +9627,7 @@ testChannelSubscriberProfileUpdate ps = ] -- known subscriber updates profile (XInfo signed) + -- cath has support chat -> profile update created in support scope threadDelay 1000000 cath ##> "/_profile 1 {\"displayName\": \"kate\", \"fullName\": \"\"}" cath <## "user profile is changed to kate (your 0 contacts are notified)" @@ -9278,9 +9638,12 @@ testChannelSubscriberProfileUpdate ps = dan <# "#team kate> hello from kate [>>]", eve <# "#team kate> hello from kate [>>]" ] - -- profile update items on alice and bob (owner/relay, signed) - alice #$> ("/_get chat #1 count=2", chat, [(0, "updated profile (signed)"), (0, "hello from kate")]) - bob #$> ("/_get chat #1 count=2", chat, [(0, "updated profile (signed)"), (0, "hello from kate")]) + -- no profile update items in main scope on alice and bob + alice #$> ("/_get chat #1 count=2", chat, [(0, "hello from cath"), (0, "hello from kate")]) + bob #$> ("/_get chat #1 count=2", chat, [(0, "hello from cath"), (0, "hello from kate")]) + -- profile update items in cath's support scope on alice and bob + alice #$> ("/_get chat #1(_support:3) count=1", chat, [(0, "updated profile (signed)")]) + bob #$> ("/_get chat #1(_support:3) count=1", chat, [(0, "updated profile (signed)")]) -- no profile update items on dan and eve (subscriber-to-subscriber muted) dan #$> ("/_get chat #1 count=2", chat, [(0, "hello from cath"), (0, "hello from kate")]) eve #$> ("/_get chat #1 count=2", chat, [(0, "hello from cath"), (0, "hello from kate")]) @@ -9293,6 +9656,7 @@ testChannelSubscriberProfileUpdate ps = eve `hasContactProfiles` ["alice", "bob", "kate", "eve"] -- previously silent subscriber updates profile + -- dan has no support chat -> no profile update item created threadDelay 1000000 dan ##> "/_profile 1 {\"displayName\": \"dave\", \"fullName\": \"\"}" dan <## "user profile is changed to dave (your 0 contacts are notified)" @@ -9307,20 +9671,324 @@ testChannelSubscriberProfileUpdate ps = cath <## "#team: bob forwarded a message from an unknown member, creating unknown member record dave" cath <# "#team dave> hello from dave [>>]" ] - -- profile update items on alice and bob (moderator+/relay, 2nd profile update signed) - alice #$> ("/_get chat #1 count=2", chat, [(0, "updated profile (signed)"), (0, "hello from dave")]) - bob #$> ("/_get chat #1 count=2", chat, [(0, "updated profile (signed)"), (0, "hello from dave")]) + -- no profile update items in main scope (dan has no support chat, item not created) + alice #$> ("/_get chat #1 count=2", chat, [(0, "hello from kate"), (0, "hello from dave")]) + bob #$> ("/_get chat #1 count=2", chat, [(0, "hello from kate"), (0, "hello from dave")]) -- no profile update items on cath and eve (subscriber-to-subscriber muted) cath #$> ("/_get chat #1 count=2", chat, [(1, "hello from kate"), (0, "hello from dave")]) eve #$> ("/_get chat #1 count=2", chat, [(0, "hello from kate"), (0, "hello from dave")]) -- dan doesn't see his own profile update dan #$> ("/_get chat #1 count=2", chat, [(0, "hello from kate"), (1, "hello from dave")]) + -- verify dan has no support chat (only kate has one) + alice ##> "/member support chats #team" + alice <## "members require attention: 0" + alice <## "kate (id 3): unread: 0, require attention: 0, mentions: 0" -- verify profiles are updated correctly forM_ [alice, bob] $ \cc -> cc `hasContactProfiles` ["alice", "bob", "kate", "dave", "eve"] cath `hasContactProfiles` ["alice", "bob", "kate", "dave"] dan `hasContactProfiles` ["alice", "bob", "kate", "dave"] eve `hasContactProfiles` ["alice", "bob", "kate", "dave", "eve"] +testChannelAddRelay :: HasCallStack => TestParams -> IO () +testChannelAddRelay ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> do + -- create channel with 1 relay (bob) + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + + -- subscriber joins through bob (the only relay at this point) + memberJoinChannel "team" [bob] [alice] shortLink fullLink dan + + -- configure cath as a second relay + cath ##> "/ad" + (cathSLink, _cLink) <- getContactLinks cath True + alice ##> ("/relays name=cath " <> cathSLink) + alice <## "ok" + + -- can't add same relay twice + alice ##> "/_add relays #1 1" + alice <## "bad chat command: some relays are already in the group" + + -- add cath relay to existing channel + alice ##> "/_add relays #1 2" + alice <## "#team: group relays:" + alice <## " - relay id 1: active" + alice <## " - relay id 2: invited" + + -- wait for cath to join as relay (async) + concurrentlyN_ + [ do + alice <## "#team: group link relays updated, current relays:" + alice + <### [ " - relay id 1: active", + " - relay id 2: active" + ] + alice <## "group link:" + void $ getTermLine alice, + cath <## "#team: you joined the group as relay" + ] + + threadDelay 100000 + + -- existing subscriber discovers and connects to new relay + dan ##> "/_get group link data #1" + dan <## "group ID: 1" + void $ getTermLine dan -- subscribers: N + concurrentlyN_ + [ do + dan <## "#team: joining the group (connecting to relay cath)..." + dan <## "#team: you joined the group (connected to relay cath)", + do + cath <## "dan (Daniel): accepting request to join group #team..." + cath <## "#team: dan joined the group" + ] + + threadDelay 100000 + + -- new subscriber joins through both relays + memberJoinChannel "team" [bob, cath] [alice] shortLink fullLink eve + + -- verify delivery through both relays + alice #> "#team hello" + [bob, cath] *<# "#team> hello" + [dan, eve] *<# "#team> hello [>>]" + +testChannelRemoveRelay :: HasCallStack => TestParams -> IO () +testChannelRemoveRelay ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel2Relays "team" alice bob cath + memberJoinChannel "team" [bob, cath] [alice] shortLink fullLink dan + + -- verify delivery works + alice #> "#team hello" + [bob, cath] *<# "#team> hello" + dan <# "#team> hello [>>]" + + -- remove relay bob + threadDelay 100000 + alice ##> "/rm #team bob" + alice <## "#team: you removed bob from the group (signed)" + concurrentlyN_ + [ do + bob <## "#team: alice removed you from the group (signed)" + bob <## "use /d #team to delete the group", + -- cath doesn't have bob in member list (relays aren't introduced to each other), + -- so x.grp.mem.del arrives with unknown member ID — cath still forwards it (Left branch in xGrpMemDel) + cath <## "error: x.grp.mem.del with unknown member ID", + dan <## "#team: alice removed bob from the group (signed)" + ] + + -- verify delivery still works via remaining relay (cath) + threadDelay 100000 + alice #> "#team still working" + cath <# "#team> still working" + dan <# "#team> still working [>>]" + + -- remove last relay cath + threadDelay 100000 + alice ##> "/rm #team cath" + alice <## "#team: you removed cath from the group (signed)" + concurrentlyN_ + [ do + cath <## "#team: alice removed you from the group (signed)" + cath <## "use /d #team to delete the group", + dan <## "#team: alice removed cath from the group (signed)" + ] + + -- verify delivery stops — no relays to forward + threadDelay 100000 + alice #> "#team no relays" + (dan + DB.query_ db "SELECT local_display_name FROM group_members" :: IO [Only T.Text] + aliceMembers `shouldMatchList` [Only "alice", Only "dan"] + danMembers <- withCCTransaction dan $ \db -> + DB.query_ db "SELECT local_display_name FROM group_members" :: IO [Only T.Text] + danMembers `shouldMatchList` [Only "dan", Only "alice"] + + -- re-add bob as relay + alice ##> "/_add relays #1 1" + alice <## "#team: group relays:" + alice .<##. (" - relay id", ": invited") + + -- wait for bob to rejoin as relay (bob gets LDN "team_1" since old group record exists) + concurrentlyN_ + [ do + alice <## "#team: group link relays updated, current relays:" + alice .<##. (" - relay id", ": active") + alice <## "group link:" + void $ getTermLine alice, + bob <## "#team_1: you joined the group as relay" + ] + + threadDelay 100000 + + -- subscriber discovers and connects to new relay + dan ##> "/_get group link data #1" + dan <## "group ID: 1" + void $ getTermLine dan -- subscribers: N + concurrentlyN_ + [ do + dan <## "#team: joining the group (connecting to relay bob)..." + dan <## "#team: you joined the group (connected to relay bob)", + do + bob <## "dan_1 (Daniel): accepting request to join group #team_1..." + bob <## "#team_1: dan_1 joined the group" + ] + + threadDelay 100000 + + -- verify delivery works again through re-added relay + alice #> "#team relays restored" + bob <# "#team_1> relays restored" + dan <# "#team> relays restored [>>]" + +testChannelRemoveLeftRelay :: HasCallStack => TestParams -> IO () +testChannelRemoveLeftRelay ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel2Relays "team" alice bob cath + memberJoinChannel "team" [bob, cath] [alice] shortLink fullLink dan + + -- verify delivery works + alice #> "#team hello" + [bob, cath] *<# "#team> hello" + dan <# "#team> hello [>>]" + + -- bob leaves + threadDelay 100000 + bob ##> "/l team" + concurrentlyN_ + [ do + bob <## "#team: you left the group" + bob <## "use /d #team to delete the group", + alice <## "#team: bob left the group (signed)", + dan <## "#team: bob left the group (signed)" + ] + + -- alice removes left bob + threadDelay 100000 + alice ##> "/rm #team bob" + alice <## "#team: you removed bob from the group (signed)" + concurrentlyN_ + [ cath <## "error: x.grp.mem.del with unknown member ID", + dan <## "#team: alice removed bob from the group (signed)" + ] + + -- bob's member record should be deleted on alice's and dan's sides + threadDelay 100000 + aliceMembers <- withCCTransaction alice $ \db -> + DB.query_ db "SELECT local_display_name FROM group_members" :: IO [Only T.Text] + aliceMembers `shouldMatchList` [Only "alice", Only "cath", Only "dan"] + danMembers <- withCCTransaction dan $ \db -> + DB.query_ db "SELECT local_display_name FROM group_members" :: IO [Only T.Text] + danMembers `shouldMatchList` [Only "dan", Only "alice", Only "cath"] + + -- cath leaves + threadDelay 100000 + cath ##> "/l team" + concurrentlyN_ + [ do + cath <## "#team: you left the group" + cath <## "use /d #team to delete the group", + alice <## "#team: cath left the group (signed)", + dan <## "#team: cath left the group (signed)" + ] + + -- alice removes left cath - dan doesn't receive (no relay to forward) + threadDelay 100000 + alice ##> "/rm #team cath" + alice <## "#team: you removed cath from the group (signed)" + + -- dan syncs with link - should clean up cath's stale record + threadDelay 100000 + dan ##> "/_get group link data #1" + dan <## "group ID: 1" + void $ getTermLine dan -- subscribers: N + + -- cath's member record should be cleaned up on dan's side after sync + threadDelay 100000 + danMembers2 <- withCCTransaction dan $ \db -> + DB.query_ db "SELECT local_display_name FROM group_members" :: IO [Only T.Text] + danMembers2 `shouldMatchList` [Only "dan", Only "alice"] + +testChannelCreateDeletedRelay :: HasCallStack => TestParams -> IO () +testChannelCreateDeletedRelay ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> do + bob ##> "/ad" + (bobSLink, _) <- getContactLinks bob True + cath ##> "/ad" + (cathSLink, _) <- getContactLinks cath True + + alice ##> ("/relays name=bob " <> bobSLink <> " name=cath " <> cathSLink) + alice <## "ok" + + -- cath deletes her address - simulates relay becoming unavailable + cath ##> "/da" + cath <## "Your chat address is deleted - accepted contacts will remain connected." + cath <## "To create a new chat address use /ad" + + -- channel creation fails because one relay's address was deleted + alice ##> "/public group relays=1,2 #team" + alice <## "channel not created, results:" + alice <## " relay 1: ok" + alice <##. " relay 2: ChatErrorAgent" + -- deleteInProgressGroup deletes relay connection alice joined on bob; + -- bob's agent reports AUTH error when the queue is gone — drain it. + void $ getTermLine bob + +testChannelSupportScope :: HasCallStack => TestParams -> IO () +testChannelSupportScope ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "relay" relayProfile $ \relay -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice relay + memberJoinChannel "team" [relay] [alice] shortLink fullLink cath + memberJoinChannel "team" [relay] [alice] shortLink fullLink dan + + threadDelay 1000000 + + alice ##> "/set support #team on" + alice <## "updated group preferences:" + alice <## "Chat with admins: on" + toggledSupport relay "alice" "team" "on" + concurrentlyN_ + [ toggledSupport cath "alice" "team" "on", + toggledSupport dan "alice" "team" "on" + ] + + -- owner sends to cath's support scope, dan doesn't receive + alice #> "#team (support: cath) hello" + relay <# "#team (support: cath) alice> hello" + cath <# "#team (support) alice> hello [>>]" + (dan "#team (support) hi" + relay <# "#team (support: cath) cath> hi" + alice <# "#team (support: cath) cath> hi [>>]" + (dan TestCC -> String -> String -> String -> IO () +toggledSupport c owner channel onOff = do + c <## (owner <> " updated group #" <> channel <> ": (signed)") + c <## "updated group preferences:" + c <## ("Chat with admins: " <> onOff) + testChannelMessageUpdate :: HasCallStack => TestParams -> IO () testChannelMessageUpdate ps = withNewTestChat ps "alice" aliceProfile $ \alice -> diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 087cd3b900..b6ab89d00a 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -81,6 +81,9 @@ frankProfile = mkProfile "frank" "Frank" Nothing businessProfile :: Profile businessProfile = mkProfile "biz" "Biz Inc" Nothing +relayProfile :: Profile +relayProfile = mkProfile "relay" "Relay" Nothing + serviceProfile :: Profile serviceProfile = mkProfile "service_user" "Service user" Nothing @@ -290,7 +293,10 @@ groupFeatures :: [(Int, String)] groupFeatures = map (\(a, _, _) -> a) $ groupFeatures'' 0 groupFeaturesNoE2E :: [(Int, String)] -groupFeaturesNoE2E = map (\(a, _, _) -> a) $ ((1, "chat banner"), Nothing, Nothing) : groupFeatures_ 0 +groupFeaturesNoE2E = map (\(a, _, _) -> a) $ ((1, "chat banner"), Nothing, Nothing) : groupFeatures_ 0 False + +channelFeaturesNoE2E :: [(Int, String)] +channelFeaturesNoE2E = map (\(a, _, _) -> a) $ ((1, "chat banner"), Nothing, Nothing) : groupFeatures_ 0 True sndGroupFeatures :: [(Int, String)] sndGroupFeatures = map (\(a, _, _) -> a) $ groupFeatures'' 1 @@ -299,10 +305,10 @@ groupFeatureStrs :: [String] groupFeatureStrs = map (\(a, _, _) -> snd a) $ groupFeatures'' 0 groupFeatures'' :: Int -> [((Int, String), Maybe (Int, String), Maybe String)] -groupFeatures'' dir = ((1, "chat banner"), Nothing, Nothing) : ((dir, e2eeInfoNoPQStr), Nothing, Nothing) : groupFeatures_ dir +groupFeatures'' dir = ((1, "chat banner"), Nothing, Nothing) : ((dir, e2eeInfoNoPQStr), Nothing, Nothing) : groupFeatures_ dir False -groupFeatures_ :: Int -> [((Int, String), Maybe (Int, String), Maybe String)] -groupFeatures_ dir = +groupFeatures_ :: Int -> Bool -> [((Int, String), Maybe (Int, String), Maybe String)] +groupFeatures_ dir isChannel = [ ((dir, "Disappearing messages: off"), Nothing, Nothing), ((dir, "Direct messages: on"), Nothing, Nothing), ((dir, "Full deletion: off"), Nothing, Nothing), @@ -311,7 +317,8 @@ groupFeatures_ dir = ((dir, "Files and media: on"), Nothing, Nothing), ((dir, "SimpleX links: on"), Nothing, Nothing), ((dir, "Member reports: on"), Nothing, Nothing), - ((dir, "Recent history: on"), Nothing, Nothing) + ((dir, "Recent history: on"), Nothing, Nothing), + ((dir, "Chat with admins: " <> (if isChannel then "off" else "on")), Nothing, Nothing) ] businessGroupFeatures :: [(Int, String)] @@ -329,7 +336,8 @@ businessGroupFeatures'' dir = ((dir, "Files and media: on"), Nothing, Nothing), ((dir, "SimpleX links: on"), Nothing, Nothing), ((dir, "Member reports: off"), Nothing, Nothing), - ((dir, "Recent history: on"), Nothing, Nothing) + ((dir, "Recent history: on"), Nothing, Nothing), + ((dir, "Chat with admins: on"), Nothing, Nothing) ] itemId :: Int -> String diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 1b708a2ffa..01399f6bbb 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -101,7 +101,7 @@ testChatPreferences :: Maybe Preferences testChatPreferences = Just Preferences {voice = Just VoicePreference {allow = FAYes}, files = Nothing, fullDelete = Nothing, timedMessages = Nothing, calls = Nothing, reactions = Just ReactionsPreference {allow = FAYes}, sessions = Nothing, commands = Nothing} testGroupPreferences :: Maybe GroupPreferences -testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, sessions = Nothing, commands = Nothing} +testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, support = Nothing, sessions = Nothing, comments = Nothing, commands = Nothing} testProfile :: Profile testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences} @@ -113,84 +113,79 @@ decodeChatMessageTest :: Spec decodeChatMessageTest = describe "Chat message encoding/decoding" $ do it "x.msg.new simple text" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" - #==# XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)) + #==# XMsgNew (mcSimple (MCText "hello")) it "x.msg.new simple text - timed message TTL" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"ttl\":3600}}" - #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") [] Nothing (Just 3600) Nothing Nothing Nothing)) + #==# XMsgNew ((mcSimple (MCText "hello")) {ttl = Just 3600}) it "x.msg.new simple text - live message" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"live\":true}}" - #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") [] Nothing Nothing (Just True) Nothing Nothing)) + #==# XMsgNew ((mcSimple (MCText "hello")) {live = Just True}) it "x.msg.new simple link" $ "{\"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)) + #==# XMsgNew (mcSimple (MCLink "https://simplex.chat" $ LinkPreview {uri = "https://simplex.chat", title = "SimpleX Chat", description = "SimpleX Chat", image = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA", content = Nothing})) it "x.msg.new simple image" $ "{\"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)) + #==# XMsgNew (mcSimple (MCImage "" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=")) it "x.msg.new simple image with text" $ "{\"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)) + #==# XMsgNew (mcSimple (MCImage "here's an image" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=")) 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))) + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello"))) it "x.msg.new chat message with chat version range" $ "{\"v\":\"1-17\",\"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))) + ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello"))) it "x.msg.new quote" $ "{\"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))) + (XMsgNew (mcQuote quotedMsg (MCText "hello to you too"))) it "x.msg.new quote - timed message TTL" $ "{\"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 Nothing Nothing))) + (XMsgNew ((mcQuote quotedMsg (MCText "hello to you too")) {ttl = Just 3600})) it "x.msg.new quote - live message" $ "{\"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) Nothing Nothing))) + (XMsgNew ((mcQuote quotedMsg (MCText "hello to you too")) {live = Just True})) it "x.msg.new forward" $ "{\"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)) + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ mcForward (MCText "hello")) it "x.msg.new forward - timed message TTL" $ "{\"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 Nothing Nothing)) + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ (mcForward (MCText "hello")) {ttl = Just 3600}) it "x.msg.new forward - live message" $ "{\"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) Nothing Nothing)) + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ (mcForward (MCText "hello")) {live = Just True}) it "x.msg.new simple text with file" $ "{\"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}))) + #==# XMsgNew ((mcSimple (MCText "hello")) {file = Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}}) it "x.msg.new simple file with file" $ "{\"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}))) + #==# XMsgNew ((mcSimple (MCFile "")) {file = Just FileInvitation {fileName = "file.txt", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}}) it "x.msg.new quote with file" $ - "{\"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\"}}}" + "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"},\"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") - (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}) - ) - ) + (mcQuote quotedMsg (MCText "hello to you too")) + {file = Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}} ) it "x.msg.new report" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"reason\":\"spam\",\"type\":\"report\"},\"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 (MCReport "" RRSpam) Nothing))) + (XMsgNew (mcQuote quotedMsg (MCReport "" RRSpam))) it "x.msg.new forward with file" $ - "{\"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}))) + "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"},\"forward\":true}}" + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ (mcForward (MCText "hello")) {file = Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}}) it "x.msg.update" $ "{\"v\":\"1\",\"event\":\"x.msg.update\",\"params\":{\"msgId\":\"AQIDBA==\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" #==# XMsgUpdate (SharedMsgId "\1\2\3\4") (MCText "hello") [] Nothing Nothing Nothing Nothing @@ -300,7 +295,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do -- $ "{\"v\":\"1\",\"event\":\"x.grp.msg.forward\",\"params\":{\"msgForward\":{\"memberId\":\"AQIDBA==\",\"msg\":\"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}\",\"msgTs\":\"1970-01-01T00:00:01.000000001Z\"}}}" -- #==# XGrpMsgForward -- (MemberId "\1\2\3\4") - -- (ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)))) + -- (ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello")))) -- (systemToUTCTime $ MkSystemTime 1 1) it "x.info.probe" $ "{\"v\":\"1\",\"event\":\"x.info.probe\",\"params\":{\"probe\":\"AQIDBA==\"}}" diff --git a/website/.eleventy.js b/website/.eleventy.js index 1a98609f1a..b02cc49e78 100644 --- a/website/.eleventy.js +++ b/website/.eleventy.js @@ -1,6 +1,7 @@ const markdownIt = require("markdown-it") const markdownItAnchor = require("markdown-it-anchor") const markdownItReplaceLink = require('markdown-it-replace-link') +const markdownItFootnote = require('markdown-it-footnote') const slugify = require("slugify") const uri = require('fast-uri') const i18n = require('eleventy-plugin-i18n') @@ -53,7 +54,7 @@ const globalConfig = { } const translationsDirectoryPath = './langs' -const supportedRoutes = ["blog", "contact", "invitation", "messaging", "docs", "fdroid", "why", ""] +const supportedRoutes = ["blog", "contact", "invitation", "messaging", "docs", "fdroid", "why", "file", ""] let supportedLangs = [] fs.readdir(translationsDirectoryPath, (err, files) => { if (err) { @@ -438,6 +439,38 @@ module.exports = function (ty) { strict: true, }) }).use(markdownItReplaceLink) + .use(markdownItFootnote) + + markdownLib.renderer.rules.footnote_anchor_name = function (tokens, idx, options, env) { + var token = tokens[idx] + var label = token.meta.label + if (label) return label + var n = Number(token.meta.id + 1).toString() + var prefix = typeof env.docId === 'string' ? '-' + env.docId + '-' : '' + return prefix + n + } + markdownLib.renderer.rules.footnote_caption = function (tokens, idx) { + var n = Number(tokens[idx].meta.id + 1).toString() + if (tokens[idx].meta.subId > 0) n += ':' + tokens[idx].meta.subId + return n + } + markdownLib.renderer.rules.footnote_ref = function (tokens, idx, options, env, slf) { + var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf) + var caption = slf.rules.footnote_caption(tokens, idx, options, env, slf) + var refid = id + if (tokens[idx].meta.subId > 0) refid += ':' + tokens[idx].meta.subId + return '' + caption + '' + } + markdownLib.renderer.rules.footnote_open = function (tokens, idx, options, env, slf) { + var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf) + if (tokens[idx].meta.subId > 0) id += ':' + tokens[idx].meta.subId + return '
  • ' + } + markdownLib.renderer.rules.footnote_anchor = function (tokens, idx, options, env, slf) { + var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf) + if (tokens[idx].meta.subId > 0) id += ':' + tokens[idx].meta.subId + return ' ↩︎' + } // replace the default markdown-it instance ty.setLibrary("md", markdownLib) diff --git a/website/README.md b/website/README.md index 5a5155a917..560b427136 100644 --- a/website/README.md +++ b/website/README.md @@ -2,8 +2,6 @@ ## License -SimpleX Chat website code is licensed under the GNU Affero General Public License version 3 (AGPLv3). See the [LICENSE](../LICENSE) file for details. +SimpleX Chat website code is licensed under the GNU Affero General Public License version 3 (AGPLv3). See the [LICENSE](./LICENSE) file for details. The SimpleX and SimpleX Chat name, logo, associated branding materials, and graphic assets (illustrations, images, visual designs, etc.) are not covered by this license and are subject to the terms outlined in the [TRADEMARK](../docs/TRADEMARK.md) and [ASSETS_LICENSE](../assets/ASSETS_LICENSE.md) files respectively. -The SimpleX and SimpleX Chat name, logo, and associated branding materials are not covered by this license and are subject to the terms outlined in the [TRADEMARK](./docs/TRADEMARK.md) file. - -Graphic designs, artworks and layouts are not licensed for re-use. If you want to use them in your publications, please ask for permission. Texts can be used as direct quotes, referencing the source. +If you want to use any graphic assets in your publications, please ask for permission. Texts can be used as direct quotes, referencing the source. diff --git a/website/langs/ar.json b/website/langs/ar.json index 41825d0683..264bf9561a 100644 --- a/website/langs/ar.json +++ b/website/langs/ar.json @@ -260,7 +260,7 @@ "about-and-contact-us": "عن واتصل بنا", "index-hero-h1": "كن
    حراً", "index-hero-h2": "في شبكتك", - "index-hero-p1": "مراسلة خاصة وآمنة.
    الشبكة الأولى التي تمتلك فيها جهات اتصالك ومجموعاتك.", + "index-hero-p1": "أول شبكة بدون معرّفات مستخدمين.
    أنت تملك جهات اتصالك ومجموعاتك وقنواتك.", "index-hero-download-desktop-btn-title": "نزّل تطبيق سطح مكتب SimpleX", "index-testflight-title": "إصدار SimpleX التجريبي ل iOS على TestFlight", "index-f-droid-title": "تطبيق SimpleX من خلال F-Droid", @@ -273,27 +273,28 @@ "index-publications-heise-title": "منشورات Heise Online", "index-publications-kuketz-title": "مراجعة بواسطة Mike Kuketz", "index-publications-optout-title": "مقابلة بودكاست OptOut", - "worlds-most-secure-messaging": "المُراسلة الأكثر أمانًا في العالم", - "index-messaging-p1": "تتميز رسائل SimpleX بتعمية متطورة بين الطرفين.", - "index-messaging-p2": "لأمانك وخصوصيتك، لا يمكن للخوادم رؤية رسائلك ومَن تتحدث إليه.", + "worlds-most-secure-messaging": "لا أحد يمكنه معرفة من تتحدث إليه", + "index-messaging-p1": "حتى الخوادم لا تستطيع ذلك – جميع الرسائل تبدو كضوضاء عشوائية.", + "index-messaging-p2": "عشرات الملايين من الرسائل تُسلَّم بخصوصية كل يوم.", "index-messaging-cta": "تعرّف على المزيد حول مراسلة SimpleX", - "index-nextweb-h2": "أنت تملك
    الشبكة التالية", - "index-nextweb-p1": "تأسست SimpleX على الاعتقاد بأن عليك امتلاك هويتك وجهات اتصالك ومجتمعاتك.", - "index-nextweb-p2": "شبكة مفتوحة ولامركزية تتيح لك التواصل مع الأشخاص ومشاركة الأفكار: كن حرًا وآمنًا.", - "index-token-h2": "مجتمعات تدوم", - "index-token-p1": "ستدعم مجموعاتك المفضلة بقسائم المجتمع المستقبلية.", - "index-token-p2": "ستدفع القسائم ثمن الخوادم، لتمكين مجتمعاتك من البقاء حرة ومستقلة.", + "index-nextweb-h2": "أنت تملك
    الشبكة", + "index-nextweb-p1": "كل جهة اتصال ومجموعة موجودة على جهازك، وليس في قاعدة بيانات خادم.", + "index-nextweb-p2": "لا يتحكم أي كيان واحد في الشبكة – يمكن لأي شخص تشغيل الخوادم.", + "index-token-h2": "يموّله مستخدموه", + "index-token-p1": "للحفاظ على الاستقلالية، ستدفع القنوات والمجتمعات الكبيرة مقابل خوادمها.", + "index-token-p2": "سيغطي ذلك البنية التحتية وتطوير البرمجيات وإدارة الشبكة.", "index-roadmap-h2": "خارطة طريق SimpleX للإنترنت المجاني", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "التوسع إلى مجتمعات كبيرة", - "index-roadmap-2025-desc": "الهروب من المنصات المركزية", - "index-roadmap-2026-title": "مجتمعات وخوادم مستدامة", - "index-roadmap-2026-desc": "إطلاق قسائم المجتمع", - "index-roadmap-2027-title": "اجعل مجتمعاتك تنمو", - "index-roadmap-2027-desc": "أدوات لتعزيز مجتمعاتك", + "index-roadmap-now": "الآن", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "التوسع إلى مجتمعات كبيرة", + "index-roadmap-1-desc": "الهروب من المنصات المركزية", + "index-roadmap-2-title": "مجتمعات وخوادم مستدامة", + "index-roadmap-2-desc": "إطلاق أرصدة المجتمع", + "index-roadmap-3-title": "اجعل مجتمعاتك تنمو", + "index-roadmap-3-desc": "أدوات لتعزيز مجتمعاتك", "index-directory-h2": "انضم إلى مجتمعات SimpleX", - "index-directory-p1": "مئات الآلاف من الأشخاص يثقون بالفعل في مُراسلة SimpleX.", - "index-directory-p2": "ابحث عن مجتمعاتك في دليل SimpleX وأنشئ مجتمعك الخاص!", + "index-directory-p1": "أكثر من 2 مليون شخص قاموا بتنزيل تطبيقات SimpleX.", + "index-directory-p2": "ابحث عن قنواتك ومجتمعاتك في الدليل وأنشئ قنواتك الخاصة!", "index-directory-cta": "اعرض دليل SimpleX", "index-directory-users-group-title": "مجموعة مستخدمي SimpleX", "how-secure-comparison-title": "مقارنة أمان التعمية بين الطرفين في برامج المُراسلة المختلفة", @@ -309,10 +310,10 @@ "messengers-comparison-section-list-point-4": "تطبيقات الأجهزة المتعددة تعرض أمن Double Ratchet بعد الاختراق للخطر", "messengers-comparison-section-list-point-5": "تبادل المفاتيح الثنائي اختياري عبر التحقق من رمز الأمان.", "messengers-comparison-section-list-point-6": "اتفاقية مفتاح ما بعد الكم \"متفرقة\" — فهي تحمي بعض خطوات ratchet فقط.", - "index-roadmap-2026": "2026", - "index-roadmap-2027": "2027", + "index-roadmap-2": "يونيو 2027", + "index-roadmap-3": "ديسمبر 2027", "navbar-token": "رمز", - "index-token-cta": "تعرف على المزيد واحصل على NFT مجاني
    للاختبار المبكر.", + "index-token-cta": "اعرف المزيد عن أرصدة المجتمع", "navbar-old-site": "الموقع القديم", "send-file": "إرسال ملف" } diff --git a/website/langs/cs.json b/website/langs/cs.json index 19f4aeedb6..0805173d37 100644 --- a/website/langs/cs.json +++ b/website/langs/cs.json @@ -6,7 +6,7 @@ "hero-overlay-card-1-p-3": "Definujete, které servery se mají používat k přijímání zpráv, vašich kontaktů — servery, které používáte k odesílání zpráv. Každá konverzace bude pravděpodobně používat dva různé servery.", "hero-overlay-card-2-p-3": "I v těch nejsoukromějších aplikacích, které používají služby Tor v3, pokud mluvíte se dvěma různými kontakty prostřednictvím stejného profilu, může být prokázáno, že jsou spojeni se stejnou osobou.", "simplex-network-overlay-card-1-p-1": "P2P protokoly a aplikace pro zasílání zpráv mají různé problémy, které je činí méně spolehlivými než SimpleX, složitějšími na analýzu a zranitelnými vůči několika typům útoků.", - "simplex-network-overlay-card-1-li-1": "P2P sítě spoléhají na nějakou variantu DHT pro směrování zpráv. Návrhy DHT musí vyvážit záruku dodávky a latenci. SimpleX má lepší záruku doručení a nižší latenci než P2P, protože zpráva může být zároveň předávána přes několik serverů pomocí serverů vybraných příjemcem. V P2P sítích je zpráva předávána přes uzly O(log N) postupně pomocí uzlů vybraných algoritmem.", + "simplex-network-overlay-card-1-li-1": "P2P sítě spoléhají na varianty DHT pro směrování zpráv. Tyto implementace DHT musí vyvážit záruku dodávky a latenci. SimpleX má lepší záruku doručení a nižší latenci než P2P, protože zpráva může být zároveň předávána přes několik serverů, které si příjemce vybere. V P2P sítích je zpráva předávána přes uzly O(log N) postupně/sekvenčně pomocí uzlů vybraných algoritmem.", "home": "Úvod", "developers": "Vývojáři", "reference": "Odkazy", @@ -193,7 +193,7 @@ "simplex-explained-tab-1-p-1": "Můžete vytvářet kontakty a skupiny a vést obousměrné konverzace, stejně jako v jakémkoli jiném messengeru.", "simplex-explained-tab-3-p-2": "Uživatelé mohou dále zlepšit soukromí metadat pomocí Tor pro přístup k serverům, což zabraňuje korelaci podle IP adresy.", "hero-p-1": "Jiné aplikace mají uživatelská ID: Signal, Matrix, Session, Briar, Jami, Cwtch atd.
    SimpleX ne, ani náhodná čísla.
    To radikálně zlepšuje vaše soukromí.", - "hero-2-header-desc": "Video ukazuje, jak se spojit se svým přítelem prostřednictvím jeho jednorázového QR kódu, osobně nebo prostřednictvím QR kódu ve videu. Můžete se také připojit sdílením pozvánky.", + "hero-2-header-desc": "Video ukazuje, jak se spojit se svým přítelem prostřednictvím jeho jednorázového QR kódu, osobně nebo prostřednictvím QR kódu ve videu. Můžete se také připojit sdílením odkazu pozvánky.", "feature-2-title": "E2E šifrované
    obrázky, videa a soubory", "feature-5-title": "Mizící tajné konverzace", "simplex-private-1-title": "2-vrstvé
    end-to-end šifrování", @@ -208,11 +208,11 @@ "if-you-already-installed-simplex-chat-for-the-terminal": "Pokud jste již nainstalovali SimpleX Chat pro terminál", "if-you-already-installed": "Pokud jste již nainstalovali", "simplex-network-1-desc": "Všechny zprávy se odesílají přes servery, což zajišťuje lepší soukromí metadat a spolehlivé asynchronní doručování zpráv, přičemž se zamezuje mnoha", - "simplex-private-card-1-point-1": "Double-ratchet protokol —
    OTR messaging s dokonalým dopředným utajením a obnovou po vloupání.", + "simplex-private-card-1-point-1": "Double-ratchet protokol —
    OTR zprávy s perfect forward secrecy a obnovou po narušení.", "guide-dropdown-1": "Rychlý start", "guide-dropdown-2": "Odesílání zpráv", "guide-dropdown-3": "Tajné skupiny", - "guide-dropdown-4": "Chat profily", + "guide-dropdown-4": "Profily chatu", "guide-dropdown-5": "Správa dat", "guide-dropdown-6": "Audio a video hovory", "guide-dropdown-7": "Soukromí a bezpečnost", @@ -236,7 +236,7 @@ "hero-overlay-3-textlink": "Hodnocení zabezpečení", "hero-overlay-card-3-p-1": "Trail of Bits je přední bezpečnostní a technologické poradenství, jejichž klienti zahrnují velké technologické firmy, vládní agentury a významné blockchainové projekty.", "f-droid-page-simplex-chat-repo-section-text": "Chcete-li jej přidat do vašeho F-Droid clienta, naskenujte QR kód nebo použijte tuto adresu URL:", - "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat a F-Droid.org repozitáře jsou podepsané různými klíči. Chcete-li přepnout, prosím exportujte chat databázi a přeinstalujte aplikaci.", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat a F-Droid.org repozitáře jsou podepsané různými klíči. Chcete-li mezi nimi přejít, prosím exportujte chat databázi a přeinstalujte aplikaci.", "comparison-section-list-point-4a": "SimpleX relé nemůže ohrozit šifrování e2e. Ověřte bezpečnostní kód, který zmírňuje mimo pásmový útok na kanál", "docs-dropdown-8": "SimpleX Directory", "please-enable-javascript": "Prosím, povolte JavaScript k zobrazení QR kódu.", @@ -261,7 +261,7 @@ "navbar-token": "Token", "index-hero-h1": "
    Žijte
    svobodně", "index-hero-h2": "Ve Své Síti", - "index-hero-p1": "Soukromé a bezpečné zasílání zpráv.
    První síť, kde vaše kontakty a skupiny patří vám.", + "index-hero-p1": "První síť bez uživatelských ID.
    Vaše kontakty, skupiny a kanály patří vám.", "index-hero-download-desktop-btn-title": "Stáhněte si desktopovou aplikaci SimpleX", "index-testflight-title": "Beta verze SimpleX pro iOS na TestFlight", "index-f-droid-title": "Stáhnout aplikaci SimpleX přes F-Droid", @@ -274,30 +274,31 @@ "index-publications-heise-title": "Publikace Heise Online", "index-publications-kuketz-title": "Rezence od Mike Kuketz", "index-publications-optout-title": "Rozhovor v podcastu OptOut", - "worlds-most-secure-messaging": "Nejbezpečnější komunikační platforma na světě", - "index-messaging-p1": "Zprávy v SimpleX jsou chráněny nejpokročilejším koncovým šifrováním (end-to-end).", - "index-messaging-p2": "Pro vaše soukromí servery nevidí vaše zprávy ani to, s kým si píšete.", + "worlds-most-secure-messaging": "Nikdo nevidí, s kým komunikujete", + "index-messaging-p1": "Ani servery – všechny zprávy vypadají jako náhodný šum.", + "index-messaging-p2": "Každý den je soukromě doručeno desítky milionů zpráv.", "index-messaging-cta": "Zjistit více o zprávách v SimpleX", - "index-nextweb-h2": "Váš internet
    budoucnosti", - "index-nextweb-p1": "SimpleX je postaven na myšlence, že vy musíte vlastnit váš profil, kontakty a komunity.", - "index-nextweb-p2": "Nikým nevlastněná decentralizovaná síť, vám umožní spojit se s lidmi a sdílet nápady, být svobodný a bezpečný ve své síti.", - "index-token-h2": "Stabilní komunity", - "index-token-p1": "Své oblíbené skupiny budete moci podpořit pomocí připravovaných Skupinových Voucherů.", - "index-token-p2": "Vouchery budou sloužit k úhradě provozu serverů, aby skupiny zůstaly svobodné a nezávislé.", - "index-token-cta": "Zjistěte více a získejte bezplatnou vstupenku pro rané testování.", + "index-nextweb-h2": "Síť je
    vaše", + "index-nextweb-p1": "Každý kontakt a skupina jsou na vašem zařízení, ne v databázi serveru.", + "index-nextweb-p2": "Síť nekontroluje žádný subjekt – servery může provozovat kdokoli.", + "index-token-h2": "Financováno uživateli", + "index-token-p1": "Pro zachování nezávislosti budou velké kanály a komunity platit za své servery.", + "index-token-p2": "To pokryje infrastrukturu, vývoj softwaru a správu sítě.", + "index-token-cta": "Zjistěte více o Community Credits", "index-roadmap-h2": "Plán SimpleX ke svobodnému internetu", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Škálování pro velké komunity", - "index-roadmap-2025-desc": "Odchod od centralizovaných platforem", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Udržitelné komunity a servery", - "index-roadmap-2026-desc": "Spuštění Skupinových Voucherů", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Pomozte své komunitě růst", - "index-roadmap-2027-desc": "Nástroje na podporu vašich komunit", + "index-roadmap-now": "Nyní", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Škálování pro velké komunity", + "index-roadmap-1-desc": "Odchod od centralizovaných platforem", + "index-roadmap-2": "Červen 2027", + "index-roadmap-2-title": "Udržitelné komunity a servery", + "index-roadmap-2-desc": "Spuštění Community Credits", + "index-roadmap-3": "Prosinec 2027", + "index-roadmap-3-title": "Pomozte své komunitě růst", + "index-roadmap-3-desc": "Nástroje na podporu vašich komunit", "index-directory-h2": "Zapojte se do komunit SimpleX", - "index-directory-p1": "Statisíce lidí už důvěřují komunikaci přes SimpleX.", - "index-directory-p2": "Najděte své komunity v katalogu SimpleX a vytvořte si vlastní!", + "index-directory-p1": "Více než 2 miliony lidí si stáhly aplikace SimpleX.", + "index-directory-p2": "Najděte své kanály a komunity v katalogu a vytvořte si vlastní!", "index-directory-cta": "Zobrazit katalog SimpleX", "index-directory-users-group-title": "Uživatelské skupiny SimpleX", "how-secure-comparison-title": "Porovnání bezpečnosti koncového šifrování (end-to-end) v různých messengerech", @@ -315,13 +316,13 @@ "messengers-comparison-section-list-point-6": "Postkvantová dohoda o klíči je omezená — chrání pouze některé kroky ratchet mechanismu.", "navbar-old-site": "Starý web", "why-p1": "Narodili jste se bez účtu.", - "why-p2": "Nikdo nesledoval vaše konverzace. Nikdo nenakreslil mapu, kde jste byli. Ochrana osobních údajů nikdy nebyla funkce — byl to způsob života.", - "why-tagline": "Buďte volní ve své síti.", - "why-footer-link": "Proč ji stavíme", + "why-p2": "Nikdo nesledoval vaše konverzace. Nikdo nevytvořil mapu, kde jste byli. Soukromí nikdy nebylo funkcí — byl to způsob života.", + "why-tagline": "Buďte svobodní ve své síti.", + "why-footer-link": "Proč ji vyvíjíme", "docs-dropdown-15": "Ověřitelné a opakovatelné sestavení", "why-p3": "Pak jsme se přesunuli na internet a každá platforma chtěla o vás něco vědět — vaše jméno, vaše číslo, vaše přátele. Smířili jsme se s tím, že cenou za komunikaci s ostatními je dát někomu vědět, s kým mluvíme. Každá generace, lidská i technická, to tak měla — telefon, e-mail, komunikátory, sociální sítě. Zdálo se, že je to jediný možný způsob.", "why-p4": "Existuje i jiný způsob. Síť bez telefonních čísel. Bez uživatelských jmen. Bez účtů. Bez jakékoli uživatelské identity. Síť, která spojuje lidi a přenáší šifrované zprávy, aniž by bylo známo, kdo je připojen.", - "why-p5": "Není lepší zámek na dveřích někoho jiného. Není milejší nájemce, který respektuje vaše soukromí, ale přesto vede evidenci všech návštěvníků. Vy nejste host. Jste doma. Ani král do něj nemůže vstoupit — jste suverén.", + "why-p5": "Nejde o to mít lepší zámek na dveřích někoho jiného. Ani o to mít nájemce, který respektuje vaše soukromí, ale vede evidenci všech vašich návštěvníků. Nejste host. Jste doma. Ani král k vám nemůže vstoupit — jste suverén.", "why-p6": "Vaše konverzace patří vám, jako tomu bylo vždy před internetem. Síť není místo, které navštěvujete. Je to místo, které vytváříte a vlastníte. A nikdo vám ho nemůže vzít, ať už je soukromé, nebo veřejné.", "why-p7": "Nejstarší lidská svoboda — mluvit s druhým člověkem, aniž by byl sledován — postavena na infrastruktuře, která ji nemůže zradit.", "why-p8": "Protože jsme zničili sílu vědět, kdo jste. Aby vám vaši moc nikdo nemohl vzít.", @@ -336,7 +337,7 @@ "file-drop-text": "Přetáhněte soubor sem", "file-drop-hint": "nebo", "file-choose": "Vyberte soubor", - "file-max-size": "Max 100 MB - SimpleX Chat apka podporuje soubory až do 1 GB", + "file-max-size": "Max. 100 MB - SimpleX Chat aplikace podporuje soubory do 1 GB", "file-encrypting": "Šifruji…", "file-uploading": "Nahrávám…", "file-cancel": "Zrušit", @@ -347,7 +348,7 @@ "file-expiry": "Soubory jsou obvykle k dispozici 48 hodin.", "file-sec-1": "Váš soubor byl zašifrován v prohlížeči - datové routery nikdy neuvidí obsah souboru, jméno nebo velikost.", "file-sec-2": "Šifrovací klíč je obsažen v hash části odkazu – nikdy se neodesílá na žádný server.", - "file-sec-3": "Pro větší bezpečnost, použijte SimpleX Chat apku.", + "file-sec-3": "Pro vyšší bezpečnost použijte aplikaci SimpleX Chat.", "file-retry": "Znovu", "file-downloading": "Stahuji…", "file-decrypting": "Dešifruji…", @@ -359,14 +360,14 @@ "file-init-error": "Chyba inicializace: %error%", "file-available": "Dostupný soubor (~%size%)", "file-dl-sec-1": "Tento soubor je šifrován - datové směrovače nikdy neuvidí obsah souboru, jméno nebo velikost.", - "file-workers-required": "Vyžadováni Web Workers — aktualizujte prohlížeč", + "file-workers-required": "Vyžadovány Web Workers — aktualizujte svůj prohlížeč", "file-protocol-title": "XFTP protokol: nejbezpečnější přenos souborů", "file-proto-h-1": "Není nutný žádný účet", "file-proto-p-1": "Každá část souborů používá nový náhodný klíč. Datové směrovače nemají \"uživatele\" nebo \"soubory\" - přenášejí šifrované části souborů stejných velikostí.", "file-proto-h-2": "Trojitě zašifrováno ve vašem prohlížeči", "file-proto-h-4": "Nezávislé směrovače dat", "file-proto-spec": "Přečtěte si specifikaci XFTP protokolu →", - "file-proto-p-2": "Šifrovací klíč souboru je obsažen pouze v části hash adresy URL – váš prohlížeč jej nikdy neodesílá na server. Existují 3 úrovně šifrování: přenos přes protokol TLS, šifrování pro každého příjemce (jedinečný dočasný klíč pro každý přenos) a šifrování souborů typu end-to-end.", + "file-proto-p-2": "Šifrovací klíč souboru je obsažen pouze v části hash adresy URL – váš prohlížeč jej nikdy neodesílá na server. Existují 3 úrovně šifrování: přenos přes protokol TLS, šifrování pro každého příjemce (jedinečný dočasný klíč pro každý přenos) a end-to-end šifrování souborů.", "file-proto-p-4": "Když je soubor rozdělen na části, je odeslán přes síťové směrovače provozované nezávislými stranami. Žádný operátor nemůže vidět aktuální velikost nebo jméno souboru. I kdyby byl směrovač ohrožen, může vidět pouze šifrované části s pevně stanovenou velikosti. Části souboru jsou v mezipaměti síťových směrovačů uchovávány přibližně 48 hodin.", "send-file": "Odeslat soubor" } diff --git a/website/langs/de.json b/website/langs/de.json index 17fb9b184a..7c52be6498 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -17,7 +17,7 @@ "simplex-explained-tab-2-p-1": "Für jede Verbindung nutzen Sie zwei separate Nachrichten-Warteschlangen, um die Nachrichten über verschiedene Server zu senden und zu empfangen.", "simplex-explained-tab-2-p-2": "Die Server leiten Nachrichten immer nur in eine Richtung weiter, ohne den vollständigen Verlauf der Nutzer-Unterhaltungen oder seiner Verbindungen zu kennen.", "simplex-explained-tab-3-p-1": "Die Server nutzen für jede Warteschlange separate, anonyme Anmeldeinformationen und wissen nicht, welchem Nutzer diese gehören.", - "simplex-explained-tab-3-p-2": "Durch die Verwendung von Tor-Zugangsservern können Nutzer ihre Metadaten-Privatsphäre weiter verbessern und Korellationen von IP-Adressen verhindern.", + "simplex-explained-tab-3-p-2": "Durch die Verwendung von Tor-Zugangsservern können Nutzer ihre Metadaten-Privatsphäre weiter verbessern und Korrelationen von IP-Adressen verhindern.", "smp-protocol": "SMP-Protokoll", "chat-bot-example": "Beispiel für einen Chatbot", "donate": "Spenden", @@ -68,7 +68,7 @@ "simplex-private-card-9-point-1": "Jede Nachrichten-Warteschlange leitet Nachrichten mit unterschiedlichen Sende- und Empfängeradressen jeweils nur in einer Richtung weiter.", "simplex-private-card-9-point-2": "Verglichen mit traditionellen Nachrichten-Brokern, werden mögliche Angriffs-Vektoren und vorhandene Metadaten reduziert.", "simplex-private-card-10-point-1": "SimpleX nutzt für jeden Nutzer-Kontakt oder jedes Gruppenmitglied eigene temporäre, anonyme und paarweise Adressen und Berechtigungsnachweise.", - "simplex-private-card-10-point-2": "SimpleX erlaubt es, Nachrichten ohne Nutzerprofil-Bezeichner zu versenden und bietet dabei bessere Metadaten-Privatsphäre an als andere Alternativen.", + "simplex-private-card-10-point-2": "SimpleX ermöglicht die Zustellung von Nachrichten ohne Nutzerprofil-Kennungen und bietet dabei bessere Metadaten-Privatsphäre als andere Alternativen.", "simplex-unique-4-title": "Sie besitzen das SimpleX-Netzwerk", "privacy-matters-1-title": "Werbung und Preisdiskriminierung", "privacy-matters-1-overlay-1-title": "Privatsphäre spart Ihnen Geld", @@ -188,7 +188,7 @@ "copy-the-command-below-text": "Kopieren Sie sich das unten genannte Kommando und nutzen Sie es im Chat:", "privacy-matters-section-subheader": "Die Wahrung der Privatsphäre Ihrer Metadaten — mit wem Sie wann Kontakt haben — schützt Sie vor:", "simplex-private-section-header": "Was macht SimpleX vertraulich", - "simplex-network-section-desc": "Durch die Kombination der Vorteile von P2P und föderierten Netzwerken stellt SimpleX die bestmögliche Privatsphäre zur Verfügung.", + "simplex-network-section-desc": "Durch die Kombination der Vorteile von P2P und föderierten Netzwerken stellt SimpleX Chat die bestmögliche Privatsphäre zur Verfügung.", "simplex-network-2-header": "Im Gegensatz zu föderierten Netzwerken", "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", @@ -197,13 +197,13 @@ "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 dem 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": "Falls Sie Interesse daran haben, aktiv bei der Entwicklung des SimpleX-Netzwerks mitzuhelfen, z.B. einen Chatbot für SimpleX App-Nutzer zu entwickeln oder die Integration der SimpleX Chat-Bibliothek in mobile Apps voranzutreiben, kontaktieren Sie uns bitte für eine weitere Beratung und Unterstützung.", - "privacy-matters-overlay-card-1-p-4": "Das SimpleX-Netzwerk schützt die Privatsphäre Ihrer Verbindungen besser als jede Alternative und verhindert vollständig, dass Ihr sozialer Graph für Unternehmen oder Organisationen verfügbar wird. Selbst wenn Anwender die in der SimpleX Chat-App vorkonfigurierten Server verwenden, kennen die Server-Betreiber die Anzahl der Benutzer oder deren Verbindungen nicht.", + "privacy-matters-overlay-card-1-p-4": "Das SimpleX-Netzwerk schützt die Privatsphäre Ihrer Verbindungen besser als jede Alternative und verhindert vollständig, dass Ihr sozialer Graph für Unternehmen oder Organisationen einsehbar wird. Selbst wenn Anwender die in der SimpleX Chat-App vorkonfigurierten Server verwenden, kennen die Server-Betreiber die Anzahl der Benutzer oder deren Verbindungen nicht.", "contact-hero-header": "Sie haben eine Adresse zur Verbindung mit SimpleX Chat erhalten", "invitation-hero-header": "Sie haben einen Einmal-Link zur Verbindung mit SimpleX Chat erhalten", "privacy-matters-overlay-card-3-p-3": "Selbst in demokratischen Ländern werden normale Menschen, auch unter Nutzung ihrer „anonymen“ Benutzerkennungen, für das, was sie online teilen, verhaftet.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat speichert alle Benutzerdaten ausschließlich auf den Endgeräten in einem portablen und verschlüsselten Datenbankformat, welches exportiert und auf jedes unterstützte Gerät übertragen werden kann.", "simplex-unique-overlay-card-2-p-2": "Auch wenn die optionale Benutzeradresse zum Versenden von Spam-Kontaktanfragen verwendet werden kann, können Sie sie ändern oder ganz löschen, ohne dass Ihre Verbindungen verloren gehen.", - "simplex-unique-overlay-card-4-p-2": "Das SimpleX-Netzwerk verwendet ein offenes Protokoll und bietet ein SDK zur Erstellung von Chatbots an. Dies ermöglicht die Erstellung von Diensten, mit denen Nutzer über SimpleX Chat-Apps interagieren können — wir sind gespannt, welche SimpleX-Dienste Sie entwickeln werden.", + "simplex-unique-overlay-card-4-p-2": "Das SimpleX-Netzwerk verwendet ein offenes Protokoll und bietet ein SDK zur Erstellung von Chatbots an. Dies ermöglicht die Erstellung von Diensten, mit denen Nutzer über SimpleX Chat-Apps interagieren können — wir freuen uns sehr darauf zu sehen, welche SimpleX-Dienste Sie entwickeln werden.", "simplex-unique-card-4-p-2": "Sie können SimpleX mit eigenen Servern oder mit den von uns zur Verfügung gestellten Servern verwenden — und sich trotzdem mit jedem Benutzer verbinden.", "why-simplex-is-unique": "Warum ist SimpleX einmalig", "contact-hero-p-1": "Die öffentlichen Schlüssel und die Adresse der Nachrichtenwarteschlange in diesem Link werden NICHT über das Netzwerk gesendet, wenn Sie diese Seite aufrufen — sie sind in dem Hash-Fragment der Link-URL enthalten.", @@ -260,7 +260,7 @@ "directory": "Verzeichnis", "index-hero-h1": "Sei
    frei", "index-hero-h2": "In Deinem Netzwerk", - "index-hero-p1": "Privater und sicherer Nachrichtenaustausch.
    Das erste Netzwerk, in dem Ihnen
    Ihre Kontakte und Gruppen gehören.", + "index-hero-p1": "Das erste Netzwerk ohne Benutzer-IDs.
    Ihre Kontakte, Gruppen und Kanäle gehören Ihnen.", "index-hero-download-desktop-btn-title": "Download der SimpleX Desktop-App", "index-testflight-title": "Öffentlicher iOS-Preview auf TestFlight", "index-f-droid-title": "SimpleX-App über das F-Droid-Repository", @@ -273,30 +273,31 @@ "index-publications-heise-title": "Veröffentlichungen von Heise Online", "index-publications-kuketz-title": "Überprüfung von Mike Kuketz", "index-publications-optout-title": "Podcast-Interview von OptOut", - "worlds-most-secure-messaging": "Das sicherste Messaging-System der Welt", - "index-messaging-p1": "SimpleX-Messaging verfügt über modernste Ende-zu-Ende-Verschlüsselung.", - "index-messaging-p2": "Zu Ihrer Sicherheit und zum Schutz Ihrer Privatsphäre können Server weder Ihre Nachrichten sehen, noch mit wem Sie kommunizieren.", + "worlds-most-secure-messaging": "Niemand kann sehen, mit wem Sie kommunizieren", + "index-messaging-p1": "Nicht einmal Server – alle Nachrichten sehen aus wie zufälliges Rauschen.", + "index-messaging-p2": "Täglich werden Dutzende Millionen Nachrichten privat zugestellt.", "index-messaging-cta": "Lernen Sie mehr über SimpleX-Messaging", - "index-nextweb-h2": "Sie besitzen die
    Zukunft des Webs", - "index-nextweb-p1": "SimpleX wurde aus der Überzeugung heraus entwickelt, dass Sie Eigentümer Ihrer Profile, Kontakte und Communitys bleiben müssen.", - "index-nextweb-p2": "Ein dezentrales Netzwerk, welches Niemanden gehört, ermöglicht es Ihnen, mit Menschen in Kontakt zu treten, Ideen auszutauschen und dabei frei und sicher in Ihrem Netzwerk zu sein.", - "index-token-h2": "Communitys, die Bestand haben", - "index-token-p1": "Sie werden Ihre Lieblingsgruppen in Zukunft mit Community-Gutscheinen unterstützen können.", - "index-token-p2": "Server werden mit Gutscheinen bezahlt, damit Ihre Communitys kostenlos und unabhängig bleiben können.", - "index-token-cta": "Erfahren Sie mehr und sichern Sie sich einen kostenlosen Zugangspass, um es frühzeitig auszuprobieren.", + "index-nextweb-h2": "Das Netzwerk
    gehört Ihnen", + "index-nextweb-p1": "Jeder Kontakt und jede Gruppe liegt auf Ihrem Gerät, nicht in einer Server-Datenbank.", + "index-nextweb-p2": "Keine einzelne Instanz kontrolliert das Netzwerk – jeder kann Server betreiben.", + "index-token-h2": "Finanziert von seinen Nutzern", + "index-token-p1": "Um unabhängig zu bleiben, werden große Kanäle und Communitys für ihre Server bezahlen.", + "index-token-p2": "Dies deckt Infrastruktur, Softwareentwicklung und Netzwerkverwaltung ab.", + "index-token-cta": "Erfahren Sie mehr über Community-Credits", "index-roadmap-h2": "SimpleX - Der Weg zum freien Internet", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Skalierung auf große Communitys", - "index-roadmap-2025-desc": "Ausstieg aus zentralisierten Plattformen", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Nachhaltige Communitys & Server", - "index-roadmap-2026-desc": "Einführung von Community-Gutscheinen", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Lassen Sie Ihre Communitys wachsen", - "index-roadmap-2027-desc": "Tools zur Förderung Ihrer Communitys", + "index-roadmap-now": "Jetzt", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Skalierung auf große Communitys", + "index-roadmap-1-desc": "Ausstieg aus zentralisierten Plattformen", + "index-roadmap-2": "Juni 2027", + "index-roadmap-2-title": "Nachhaltige Communitys & Server", + "index-roadmap-2-desc": "Einführung von Community-Credits", + "index-roadmap-3": "Dez 2027", + "index-roadmap-3-title": "Lassen Sie Ihre Communitys wachsen", + "index-roadmap-3-desc": "Tools zur Förderung Ihrer Communitys", "index-directory-h2": "Treten Sie SimpleX-Communitys bei", - "index-directory-p1": "Hunderttausende Nutzer vertrauen bereits dem Messaging per SimpleX.", - "index-directory-p2": "Finden Sie Communitys im SimpleX-Verzeichnis oder erstellen Sie Ihre Eigenen!", + "index-directory-p1": "Mehr als 2 Millionen Menschen haben SimpleX-Apps heruntergeladen.", + "index-directory-p2": "Finden Sie Kanäle und Communitys im Verzeichnis oder erstellen Sie Ihre Eigenen!", "index-directory-cta": "SimpleX-Verzeichnis anzeigen", "index-directory-users-group-title": "SimpleX-Nutzergruppe", "how-secure-comparison-title": "Sicherheitsvergleich der Ende-zu-Ende-Verschlüsselung in verschiedenen Messengern", @@ -336,7 +337,7 @@ "file-drop-text": "Datei per Drag & Drop hinzufügen", "file-drop-hint": "oder", "file-choose": "Datei auswählen", - "file-max-size": "Max. 100 MB — die SimpleX Chat App unterstützt Dateien bis zu 1 GB", + "file-max-size": "Max. 100 MB — die SimpleX Chat App unterstützt Dateien bis zu 1 GB", "file-encrypting": "Wird verschlüsselt…", "file-uploading": "Wird hochgeladen…", "file-cancel": "Abbrechen", @@ -347,7 +348,7 @@ "file-expiry": "Dateien sind in der Regel 48 Stunden lang verfügbar.", "file-sec-1": "Ihre Datei wurde im Browser verschlüsselt — Datenrouter sehen weder Inhalt, Namen noch Größe der Datei.", "file-sec-2": "Der für die Verschlüsselung genutzte Schlüssel befindet sich im Hash‑Fragment des Links und wird niemals an einen Server übertragen.", - "file-sec-3": "Für noch mehr Sicherheit verwenden Sie die SimpleX Chat App.", + "file-sec-3": "Für noch mehr Sicherheit verwenden Sie die SimpleX Chat App.", "file-retry": "Wiederholen", "file-downloading": "Wird heruntergeladen…", "file-decrypting": "Wird entschlüsselt…", diff --git a/website/langs/en.json b/website/langs/en.json index aea1d057d4..490e693f18 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -18,7 +18,7 @@ "simplex-explained-tab-2-p-1": "For each connection you use two separate messaging queues to send and receive messages via different servers.", "simplex-explained-tab-2-p-2": "Servers only pass messages one way, without having the full picture of user's conversations or connections.", "simplex-explained-tab-3-p-1": "The servers have separate anonymous credentials for each queue, and do not know which users they belong to.", - "simplex-explained-tab-3-p-2": "Users can further improve metadata privacy by using Tor to access servers, preventing corellation by IP address.", + "simplex-explained-tab-3-p-2": "Users can further improve metadata privacy by using Tor to access servers, preventing correlation by IP address.", "chat-bot-example": "Chat bot example", "smp-protocol": "SMP protocol", "chat-protocol": "Chat protocol", @@ -76,7 +76,7 @@ "simplex-private-card-9-point-1": "Each message queue passes messages in one direction, with the different send and receive addresses.", "simplex-private-card-9-point-2": "It reduces the attack vectors, compared with traditional message brokers, and available meta-data.", "simplex-private-card-10-point-1": "SimpleX uses temporary anonymous pairwise addresses and credentials for each user contact or group member.", - "simplex-private-card-10-point-2": "It allows to deliver messages without user profile identifiers, providing better meta-data privacy than alternatives.", + "simplex-private-card-10-point-2": "It allows messages to be delivered without user profile identifiers, providing better meta-data privacy than alternatives.", "privacy-matters-1-title": "Advertising and price discrimination", "privacy-matters-1-overlay-1-title": "Privacy saves you money", "privacy-matters-1-overlay-1-linkText": "Privacy saves you money", @@ -113,13 +113,13 @@ "simplex-network-overlay-card-1-li-3": "P2P does not solve MITM attack problem, and most existing implementations do not use out-of-band messages for the initial key exchange. SimpleX uses out-of-band messages or, in some cases, pre-existing secure and trusted connections for the initial key exchange.", "simplex-network-overlay-card-1-li-4": "P2P implementations can be blocked by some Internet providers (like BitTorrent). SimpleX is transport agnostic — it can work over standard web protocols, e.g. WebSockets.", "simplex-network-overlay-card-1-li-5": "All known P2P networks may be vulnerable to Sybil attack, because each node is discoverable, and the network operates as a whole. Known measures to mitigate it require either a centralized component or expensive proof of work. SimpleX network has no server discoverability, it is fragmented and operates as multiple isolated sub-networks, making network-wide attacks impossible.", - "simplex-network-overlay-card-1-li-6": "P2P networks may be vulnerable to DRDoS attack, when the clients can rebroadcast and amplify traffic, resulting in network-wide denial of service. SimpleX clients only relay traffic from known connection and cannot be used by an attacker to amplify the traffic in the whole network.", + "simplex-network-overlay-card-1-li-6": "P2P networks may be vulnerable to DRDoS attack, when the clients can rebroadcast and amplify traffic, resulting in network-wide denial of service. SimpleX clients only relay traffic from known connections and cannot be used by an attacker to amplify the traffic in the whole network.", "privacy-matters-overlay-card-1-p-1": "Many large companies use information about who you are connected with to estimate your income, sell you the products you don't really need, and to determine the prices.", "privacy-matters-overlay-card-1-p-2": "Online retailers know that people with lower incomes are more likely to make urgent purchases, so they may charge higher prices or remove discounts.", "privacy-matters-overlay-card-1-p-3": "Some financial and insurance companies use social graphs to determine interest rates and premiums. It often makes people with lower incomes pay more — it is known as \"poverty premium\".", - "privacy-matters-overlay-card-1-p-4": "SimpleX network protects the privacy of your connections better than any alternative, fully preventing your social graph becoming available to any companies or organizations. Even when people use servers preconfigured in SimpleX Chat apps, server operators do not know the number of users or their connections.", + "privacy-matters-overlay-card-1-p-4": "SimpleX network protects the privacy of your connections better than any alternative, fully preventing your social graph from becoming available to any companies or organizations. Even when people use servers preconfigured in SimpleX Chat apps, server operators do not know the number of users or their connections.", "privacy-matters-overlay-card-2-p-1": "Not so long ago we observed the major elections being manipulated by a reputable consulting company that used our social graphs to distort our view of the real world and manipulate our votes.", - "privacy-matters-overlay-card-2-p-2": "To be objective and to make independent decisions you need to be in control of your information space. It is only possible if you use private communication network that does not have access to your social graph.", + "privacy-matters-overlay-card-2-p-2": "To be objective and to make independent decisions you need to be in control of your information space. It is only possible if you use a private communication network that does not have access to your social graph.", "privacy-matters-overlay-card-2-p-3": "SimpleX is the first network that doesn't have any user identifiers by design, in this way protecting your connections graph better than any known alternative.", "privacy-matters-overlay-card-3-p-1": "Everyone should care about privacy and security of their communications — harmless conversations can put you in danger, even if you have nothing to hide.", "privacy-matters-overlay-card-3-p-2": "One of the most shocking stories is the experience of Mohamedou Ould Salahi described in his memoir and shown in The Mauritanian movie. He was put into Guantanamo camp, without trial, and was tortured there for 15 years after a phone call to his relative in Afghanistan, under suspicion of being involved in 9/11 attacks, even though he lived in Germany for the previous 10 years.", @@ -135,7 +135,7 @@ "simplex-unique-overlay-card-3-p-3": "Unlike federated networks servers (email, XMPP or Matrix), SimpleX servers don't store user accounts, they only relay messages, protecting the privacy of both parties.", "simplex-unique-overlay-card-3-p-4": "There are no identifiers or ciphertext in common between sent and received server traffic — if anybody is observing it, they cannot easily determine who communicates with whom, even if TLS is compromised.", "simplex-unique-overlay-card-4-p-1": "You can use SimpleX with your own servers and still communicate with people who use the servers preconfigured in the apps.", - "simplex-unique-overlay-card-4-p-2": "SimpleX network uses an open protocol and provides SDK to create chat bots, allowing implementation of services that users can interact with via SimpleX Chat apps — we're really looking forward to see what SimpleX services you will build.", + "simplex-unique-overlay-card-4-p-2": "SimpleX network uses an open protocol and provides SDK to create chat bots, allowing implementation of services that users can interact with via SimpleX Chat apps — we're really looking forward to seeing what SimpleX services you will build.", "simplex-unique-overlay-card-4-p-3": "If you are considering developing for the SimpleX network, for example, the chat bot for SimpleX app users, or the integration of the SimpleX Chat library into your mobile apps, please get in touch for any advice and support.", "simplex-unique-card-1-p-1": "SimpleX protects the privacy of your profile, contacts and metadata, hiding it from SimpleX network servers and any observers.", "simplex-unique-card-1-p-2": "Unlike any other existing messaging network, SimpleX has no identifiers assigned to the users — not even random numbers.", @@ -166,7 +166,7 @@ "to-make-a-connection": "To make a connection:", "install-simplex-app": "Install SimpleX app", "connect-in-app": "Connect in app", - "open-simplex-app": "Open Simplex app", + "open-simplex-app": "Open SimpleX app", "tap-the-connect-button-in-the-app": "Tap the \"connect\" button in the app", "scan-the-qr-code-with-the-simplex-chat-app": "Scan the QR code with the SimpleX Chat app", "scan-the-qr-code-with-the-simplex-chat-app-description": "The public keys and message queue address in this link are NOT sent over the network when you view this page —
    they are contained in the hash fragment of the link URL.", @@ -262,7 +262,7 @@ "please-use-link-in-mobile-app": "Please use the link in the mobile app", "index-hero-h1": "Be
    Free", "index-hero-h2": "In Your Network", - "index-hero-p1": "Private and secure messaging.
    The first network where you own
    your contacts and groups.", + "index-hero-p1": "The first network without user IDs.
    You own your contacts, groups and channels.", "index-hero-download-desktop-btn-title": "Download SimpleX Desktop App", "index-testflight-title": "SimpleX iOS beta-release on TestFlight", "index-f-droid-title": "SimpleX app via F-Droid", @@ -275,30 +275,31 @@ "index-publications-heise-title": "Heise Online publications", "index-publications-kuketz-title": "Review by Mike Kuketz", "index-publications-optout-title": "OptOut podcast interview", - "worlds-most-secure-messaging": "World's Most Secure Messaging", - "index-messaging-p1": "SimpleX messaging has cutting-edge end-to-end encryption.", - "index-messaging-p2": "For your security and privacy, servers can't see your messages and who you talk to.", + "worlds-most-secure-messaging": "Nobody Can See Who You Talk To", + "index-messaging-p1": "Not even servers – all messages look like random noise.", + "index-messaging-p2": "Tens of millions of messages delivered privately every day.", "index-messaging-cta": "Learn more about SimpleX messaging", - "index-nextweb-h2": "You Own
    The Next Web", - "index-nextweb-p1": "SimpleX is created on the belief that you must own your profiles, contacts and communities.", - "index-nextweb-p2": "A decentralized network nobody owns lets you connect with people and share ideas, to be free and secure in your network.", - "index-token-h2": "Communities That Last", - "index-token-p1": "You will support your favorite groups with future Community Vouchers.", - "index-token-p2": "Vouchers will pay for servers, to let your communities stay free and independent.", - "index-token-cta": "Learn more and get a free access pass for early testing.", + "index-nextweb-h2": "You Own
    The Network", + "index-nextweb-p1": "Every contact and group is on your device, not in a server's database.", + "index-nextweb-p2": "No single entity controls the network – anyone can run servers.", + "index-token-h2": "Funded by Its Users", + "index-token-p1": "To stay independent, large channels and communities will pay for their servers.", + "index-token-p2": "This will cover infrastructure, software development and network governance.", + "index-token-cta": "Learn more about Community Credits", "index-roadmap-h2": "SimpleX Roadmap to Free Internet", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Scale to Large Communities", - "index-roadmap-2025-desc": "Escaping centralized platforms", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Sustainable Communities & Servers", - "index-roadmap-2026-desc": "Launching Community Vouchers", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Make Your Communities Grow", - "index-roadmap-2027-desc": "Tools to promote your communities", + "index-roadmap-now": "Now", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Scale to Large Communities", + "index-roadmap-1-desc": "Escaping centralized platforms", + "index-roadmap-2": "Jun 2027", + "index-roadmap-2-title": "Sustainable Communities & Servers", + "index-roadmap-2-desc": "Launching Community Credits", + "index-roadmap-3": "Dec 2027", + "index-roadmap-3-title": "Make Your Communities Grow", + "index-roadmap-3-desc": "Tools to promote your communities", "index-directory-h2": "Join SimpleX Communities", - "index-directory-p1": "Hundreds of thousands people already trust SimpleX messaging.", - "index-directory-p2": "Find your communities in SimpleX directory and create your own!", + "index-directory-p1": "More than 2 million people downloaded SimpleX apps.", + "index-directory-p2": "Find your channels and communities in directory and create your own!", "index-directory-cta": "View SimpleX Directory", "index-directory-users-group-title": "SimpleX users group", "how-secure-comparison-title": "Comparison of end-to-end encryption security in different messengers", diff --git a/website/langs/es.json b/website/langs/es.json index 5c708ef681..a294c683f6 100644 --- a/website/langs/es.json +++ b/website/langs/es.json @@ -88,7 +88,7 @@ "hero-overlay-card-2-p-2": "A continuación podrían correlacionar esta información con las redes sociales públicas existentes y averiguar las identidades reales de los usuarios.", "hero-overlay-card-2-p-3": "Incluso con las aplicaciones más privadas que usan los servicios de Tor v3, si hablas con dos contactos a través de un mismo perfil se puede probar que estos están conectados con la misma persona.", "privacy-matters-2-overlay-1-linkText": "La privacidad te da poder", - "simplex-private-card-10-point-2": "Esto permite entregar mensajes sin identificadores del perfil de usuario y proporcionar mayor privacidad de metadatos que las alternativas.", + "simplex-private-card-10-point-2": "Permite la entrega de mensajes sin identificadores del perfil de usuario y proporciona mayor privacidad de metadatos que las alternativas.", "privacy-matters-3-overlay-1-title": "La privacidad protege tu libertad", "simplex-unique-1-overlay-1-title": "Privacidad total de tu identidad, perfil, contactos y metadatos", "privacy-matters-2-title": "Manipulación de elecciones", @@ -115,7 +115,7 @@ "simplex-unique-overlay-card-3-p-2": "Los mensajes cifrados de extremo a extremo (E2E) se mantienen temporalmente en los servidores SimpleX hasta que se entregan al destinatario, y después se borran definitivamente.", "simplex-network-overlay-card-1-li-1": "Para enrutar mensajes, las redes P2P se basan en alguna variante de DHT. Los diseños DHT tienen que equilibrar la garantía de entrega y latencia. SimpleX ofrece mayor garantía de entrega y menor latencia que P2P ya que el mensaje puede transmitirse en paralelo y de forma redundante a través de varios servidores elegidos por el destinatario. En las redes P2P el mensaje se transmite secuencialmente por O(log N) nodos, usando nodos elegidos por el algoritmo.", "simplex-network-overlay-card-1-li-5": "Todas las redes P2P conocidas pueden ser vulnerables al ataque Sybil porque cada nodo es susceptible de ser descubierto y la red funciona como una unidad. Las medidas de mitigación conocidas requieren un componente centralizado o bien costosas pruebas de trabajo. La red SimpleX no tiene la capacidad para descubrir los servidores, está fragmentada y funciona como múltiples subredes aisladas haciendo imposibles los ataques a toda la red.", - "simplex-network-overlay-card-1-li-6": "Las redes P2P pueden ser vulnerables al ataque DRDoS, en el que los clientes retransmiten y amplifican el tráfico provocando el bloqueo del servicio en la red. Los clientes SimpleX sólo transmiten el tráfico desde una conexión conocida y no pueden ser usados por un atacante para amplificar el tráfico en toda la red.", + "simplex-network-overlay-card-1-li-6": "Las redes P2P pueden ser vulnerables al ataque DRDoS, en el que los clientes retransmiten y amplifican el tráfico provocando el bloqueo del servicio en la red. Los clientes SimpleX sólo transmiten el tráfico desde conexiones conocidas y no pueden ser usados por un atacante para amplificar el tráfico en toda la red.", "privacy-matters-overlay-card-1-p-1": "Las grandes empresas usan la información relativa a tus contactos para calcular tus ingresos, venderte productos que realmente no necesitas y para determinar sus precios.", "privacy-matters-overlay-card-1-p-2": "Los minoristas en línea saben que las personas con menos ingresos son más propensas a hacer compras urgentes, por lo que pueden cobrar precios más altos o eliminar descuentos.", "privacy-matters-overlay-card-1-p-3": "Algunas compañías financieras y de seguros usan las gráficas sociales para determinar primas y tipos de interés. Eso genera a menudo un mayor desembolso a personas con menores ingresos, conocido como \"prima de pobreza\".", @@ -147,7 +147,7 @@ "contact-hero-p-3": "Usa los siguientes enlaces para descargar la aplicación.", "install-simplex-app": "Instalar SimpleX Chat", "connect-in-app": "Conectar en la aplicación", - "open-simplex-app": "Abrir la aplicación Simplex", + "open-simplex-app": "Abrir la app SimpleX", "tap-the-connect-button-in-the-app": "Pulsa el botón \"conectar\" en la aplicación", "scan-the-qr-code-with-the-simplex-chat-app": "Escanear el código QR con la aplicación SimpleX Chat", "installing-simplex-chat-to-terminal": "Instalación de chat SimpleX en el terminal", @@ -184,7 +184,7 @@ "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 disponen de una autoridad central, o bien la red completa podría verse comprometida", "see-here": "ver aquí", - "simplex-unique-overlay-card-4-p-2": "La red SimpleX usa un protocolo abierto y proporciona el SDK para crear chatbots, permitiendo implementar servicios con los que los usuarios interactúen con las aplicaciones SimpleX Chat — esperamos con interés ver los servicios SimpleX que creará usted.", + "simplex-unique-overlay-card-4-p-2": "La red SimpleX usa un protocolo abierto y proporciona el SDK para crear chatbots, permitiendo implementar servicios con los que los usuarios interactúen con las aplicaciones SimpleX Chat — esperamos ver con interés los servicios SimpleX que creará usted.", "simplex-unique-overlay-card-4-p-3": "Si estás considerando desarrollar para la red SimpleX, por ejemplo un chatbot para los usuarios de SimpleX o la integración de las librerías SimpleX Chat en tus aplicaciones móviles, por favor ponte en contacto para consejos y soporte.", "simplex-unique-card-1-p-2": "A diferencia de cualquier otra red de mensajería existente, SimpleX no tiene identificadores asignados a los usuarios, ni siquiera números aleatorios.", "simplex-unique-card-2-p-1": "Al no tener identificadores o dirección permanente en la red SimpleX, nadie puede ponerse en contacto contigo salvo que compartas una dirección de usuario de un solo uso o una dirección temporal en forma de enlace o código QR.", @@ -260,7 +260,7 @@ "about-and-contact-us": "Quiénes somos & Contacto", "index-hero-h1": "Sé
    Libre", "index-hero-h2": "En Tu Red", - "index-hero-p1": "Mensajería privada y segura.
    La primera red donde
    tus contactos y grupos te pertenecen.", + "index-hero-p1": "La primera red sin IDs de usuario.
    Tus contactos, grupos y canales te pertenecen.", "index-hero-download-desktop-btn-title": "Descargar SimpleX Desktop App", "index-testflight-title": "Betas SimpleX para iOS en TestFlight", "index-f-droid-title": "SimpleX app vía F-Droid", @@ -273,30 +273,31 @@ "index-publications-heise-title": "Publicaciones Heise Online", "index-publications-kuketz-title": "Revisión por Mike Kuketz", "index-publications-optout-title": "Entrevista en podcast de OptOut", - "worlds-most-secure-messaging": "Mensajería Más Segura Del Mundo", - "index-messaging-p1": "La mensajería SimpleX posee un cifrado de extremo a extremo de vanguardia.", - "index-messaging-p2": "Para tu seguridad y privacidad los servidores no pueden ver tus mensajes ni con quién te comunicas.", + "worlds-most-secure-messaging": "Nadie Puede Ver Con Quién Hablas", + "index-messaging-p1": "Ni siquiera los servidores – todos los mensajes parecen ruido aleatorio.", + "index-messaging-p2": "Decenas de millones de mensajes entregados de forma privada cada día.", "index-messaging-cta": "Descubre más sobre la mensajería SimpleX", - "index-nextweb-h2": "La Web
    Del Futuro
    Te Pertenece", - "index-nextweb-p1": "SimpleX se origina en la creencia de que debes ser el propietario de tus perfiles, contactos y comunidades.", - "index-nextweb-p2": "Una red descentralizada que no pertenece a nadie, que te permite conectar con personas y compartir ideas de manera libre y segura en tu red.", - "index-token-h2": "Comunidades Duraderas", - "index-token-p1": "Podrás apoyar a tus grupos favoritos con los futuros Vales Comunitarios.", - "index-token-p2": "Los vales costearán los servidores para que tus comunidades sigan siendo libres e independientes.", - "index-token-cta": "Descubre más y obtén acceso gratuito para participar en las pruebas iniciales.", + "index-nextweb-h2": "La Red
    Te Pertenece", + "index-nextweb-p1": "Cada contacto y grupo está en tu dispositivo, no en la base de datos de un servidor.", + "index-nextweb-p2": "Ninguna entidad controla la red – cualquiera puede ejecutar servidores.", + "index-token-h2": "Financiada por Sus Usuarios", + "index-token-p1": "Para mantenerse independientes, los canales y comunidades grandes pagarán por sus servidores.", + "index-token-p2": "Esto cubrirá infraestructura, desarrollo de software y gobernanza de la red.", + "index-token-cta": "Descubre más sobre los Créditos Comunitarios", "index-roadmap-h2": "Ruta SimpleX hacía el Internet Libre", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Escalar a Comunidades Grandes", - "index-roadmap-2025-desc": "Huir de plataformas centralizadas", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Comunidades y Servidores Sostenibles", - "index-roadmap-2026-desc": "Lanzamiento de Vales Comunitarios", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Haz Crecer Tus Comunidades", - "index-roadmap-2027-desc": "Herramientas para la promoción", + "index-roadmap-now": "Ahora", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Escalar a Comunidades Grandes", + "index-roadmap-1-desc": "Huir de plataformas centralizadas", + "index-roadmap-2": "Jun 2027", + "index-roadmap-2-title": "Comunidades y Servidores Sostenibles", + "index-roadmap-2-desc": "Lanzamiento de Créditos Comunitarios", + "index-roadmap-3": "Dic 2027", + "index-roadmap-3-title": "Haz Crecer Tus Comunidades", + "index-roadmap-3-desc": "Herramientas para la promoción", "index-directory-h2": "Únete a las Comunidades SimpleX", - "index-directory-p1": "Miles de personas confían ya en la mensajería SimpleX.", - "index-directory-p2": "¡Encuentra comunidades en el directorio SimpleX y crea la tuya!", + "index-directory-p1": "Más de 2 millones de personas descargaron las aplicaciones SimpleX.", + "index-directory-p2": "¡Encuentra tus canales y comunidades en el directorio y crea los tuyos!", "index-directory-cta": "Ir al Directorio SimpleX", "index-directory-users-group-title": "SimpleX users group", "how-secure-comparison-title": "Comparativa del cifrado de extremo a extremo en los distintos mensajeros", @@ -335,7 +336,7 @@ "file-drop-text": "Arrastra y suelta el archivo aquí", "file-drop-hint": "o", "file-choose": "Seleccionar archivo", - "file-max-size": "Max 100 MB - La app SimpleX Chat soporta archivos hasta 1GB", + "file-max-size": "Max 100 MB - La app SimpleX Chat soporta archivos hasta 1GB", "file-encrypting": "Cifrando…", "file-uploading": "Subiendo…", "file-cancel": "Cancelar", @@ -346,7 +347,7 @@ "file-expiry": "Normalmente los archivos están disponibles durante 48 horas.", "file-sec-1": "Tu archivo se ha cifrado en el navegador, los routers de datos nunca ven el contenido, el nombre o el tamaño.", "file-sec-2": "La clave de cifrado está en el fragmento hash del enlace, nunca se envía a ningún servidor.", - "file-sec-3": "Para mejor seguridad, usa la app SimpleX Chat.", + "file-sec-3": "Para mejor seguridad, usa la app SimpleX Chat.", "file-retry": "Reintentar", "file-downloading": "Descargando…", "file-decrypting": "Descifrando…", diff --git a/website/langs/fr.json b/website/langs/fr.json index b2551990fd..765e3feec8 100644 --- a/website/langs/fr.json +++ b/website/langs/fr.json @@ -262,7 +262,7 @@ "about-and-contact-us": "À propos & Contactez-nous", "index-hero-h1": "Soyez
    Libre", "index-hero-h2": "Dans Votre Réseau", - "index-hero-p1": "Messagerie privée et sécurisée.
    Le premier réseau où vous possédez vos contacts et vos groupes.", + "index-hero-p1": "Le premier réseau sans identifiants d'utilisateur.
    Vos contacts, groupes et canaux vous appartiennent.", "index-hero-download-desktop-btn-title": "Téléchargez l’application de bureau SimpleX", "index-testflight-title": "Version bêta de SimpleX pour iOS sur TestFlight", "index-f-droid-title": "Application SimpleX via F-Droid", @@ -273,13 +273,23 @@ "index-publications-privacy-guides-title": "Messagerie recommandée par Privacy Guides", "index-publications-whonix-title": "Messagerie recommandée par Whonix", "index-publications-kuketz-title": "Analyse de Mike Kuketz", - "index-messaging-p1": "La messagerie SimpleX utilise un chiffrement de bout en bout à la pointe de la technologie.", + "worlds-most-secure-messaging": "Personne ne voit avec qui vous parlez", + "index-messaging-p1": "Même pas les serveurs – tous les messages ressemblent à du bruit aléatoire.", "index-messaging-cta": "En savoir plus sur la messagerie SimpleX", - "index-nextweb-h2": "Prenez le contrôle
    du Web de demain", - "index-nextweb-p2": "Un réseau ouvert et décentralisé vous permet de communiquer avec les autres et de partager vos idées : soyez libre et en sécurité.", - "index-token-h2": "Des communautés qui durent", - "index-token-cta": "En savoir plus et obtenez votre NFT gratuit
    pour les premiers tests.", + "index-nextweb-h2": "Le réseau
    vous appartient", + "index-nextweb-p2": "Aucune entité ne contrôle le réseau – n'importe qui peut héberger des serveurs.", + "index-token-h2": "Financé par ses utilisateurs", + "index-token-cta": "En savoir plus sur les Crédits Communautaires", "index-roadmap-h2": "Feuille de route de SimpleX vers un Internet libre", - "index-roadmap-2025": "2025", + "index-roadmap-now": "Maintenant", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Mise à l'échelle pour les grandes communautés", + "index-roadmap-1-desc": "Quitter les plateformes centralisées", + "index-roadmap-2": "Juin 2027", + "index-roadmap-2-title": "Communautés et serveurs durables", + "index-roadmap-2-desc": "Lancement des Crédits Communautaires", + "index-roadmap-3": "Déc 2027", + "index-roadmap-3-title": "Faites grandir vos communautés", + "index-roadmap-3-desc": "Outils pour promouvoir vos communautés", "send-file": "Envoyer un fichier" } diff --git a/website/langs/hu.json b/website/langs/hu.json index 5dda790d96..67133b8b80 100644 --- a/website/langs/hu.json +++ b/website/langs/hu.json @@ -11,7 +11,7 @@ "simplex-explained-tab-1-text": "1. Felhasználói élmény", "simplex-explained-tab-2-text": "2. Hogyan működik", "simplex-explained-tab-3-text": "3. Mit látnak a kiszolgálók", - "simplex-explained-tab-1-p-1": "Csoportokat hozhat létre, valamint kétirányú beszélgetéseket folytathat a partnereivel, ugyanúgy mint bármely más üzenetváltó-alkalmazásban.", + "simplex-explained-tab-1-p-1": "Csoportokat hozhat létre, valamint kétirányú beszélgetéseket folytathat a partnereivel, ugyanúgy mint bármely más üzenetváltó alkalmazásban.", "simplex-explained-tab-1-p-2": "Hogyan működhet egyirányú várólistával és felhasználói profilazonosítók nélkül?", "simplex-explained-tab-2-p-1": "Minden kapcsolathoz két különböző üzenetküldési várólistát használ a különböző kiszolgálókon keresztül történő üzenetküldéshez és -fogadáshoz.", "simplex-explained-tab-2-p-2": "A kiszolgálók csak egyetlen irányba továbbítják az üzeneteket, anélkül, hogy teljes képet kapnának a felhasználók beszélgetéseiről vagy kapcsolatairól.", @@ -21,11 +21,11 @@ "chat-protocol": "Csevegési protokoll", "donate": "Adományozás", "copyright-label": "© 2020-2025 SimpleX Chat | Nyílt forráskódú projekt", - "simplex-chat-protocol": "A SimpleX Chat protokoll", + "simplex-chat-protocol": "SimpleX Chat protokoll", "terminal-cli": "Terminál CLI", "terms-and-privacy-policy": "Adatvédelmi irányelvek", "hero-header": "Újraértelmezett adatvédelem", - "hero-subheader": "Az első üzenetváltó-alkalmazás
    felhasználói azonosítók nélkül", + "hero-subheader": "Az első üzenetváltó alkalmazás
    felhasználói azonosítók nélkül", "hero-p-1": "Más alkalmazások felhasználói azonosítókkal rendelkeznek: Signal, Matrix, Session, Briar, Jami, Cwtch, stb.
    A SimpleX azonban nem, még véletlenszerű számokkal sem.
    Ez radikálisan javítja az adatvédelmet.", "hero-overlay-1-textlink": "Miért ártanak a felhasználói azonosítók az adatvédelemnek?", "hero-overlay-2-textlink": "Hogyan működik a SimpleX?", @@ -49,7 +49,7 @@ "simplex-private-4-title": "Hozzáférés a Tor hálózaton keresztül
    (nem kötelező)", "simplex-private-5-title": "Többrétegű
    tartalomkitöltés", "simplex-private-6-title": "Sávon kívüli
    kulcscsere", - "simplex-private-7-title": "Üzenetintegritás
    hitelesítés", + "simplex-private-7-title": "Üzenetintegritás
    ellenőrzés", "simplex-private-8-title": "Üzenetek keverése
    a korreláció csökkentése érdekében", "simplex-private-9-title": "Egyirányú
    várólista az üzenetekhez", "simplex-private-10-title": "Ideiglenes, névtelen, páronkénti azonosítók", @@ -66,7 +66,7 @@ "simplex-private-card-7-point-2": "Ha bármilyen üzenetet hozzáadnak, eltávolítanak vagy módosítanak, a címzett értesítést kap róla.", "simplex-private-card-8-point-1": "A SimpleX kiszolgálók alacsony késleltetésű keverési csomópontokként működnek — a bejövő és kimenő üzenetek sorrendje eltérő.", "simplex-private-card-9-point-1": "Minden várólista az üzenetekhez egy irányba továbbítja az üzeneteket, a különböző küldési és fogadási címeken.", - "simplex-private-card-9-point-2": "Kevesebb támadási felülettel rendelkezik, mint a hagyományos üzenetváltó-alkalmazások, és kevesebb metaadatot tesz elérhetővé.", + "simplex-private-card-9-point-2": "Kevesebb támadási felülettel rendelkezik, mint a hagyományos üzenetváltó alkalmazások, és kevesebb metaadatot tesz elérhetővé.", "simplex-private-card-10-point-1": "A SimpleX ideiglenes, névtelen, páros címeket és hitelesítő adatokat használ minden egyes felhasználói kapcsolathoz vagy csoporttaghoz.", "simplex-private-card-10-point-2": "Lehetővé teszi az üzenetek felhasználói profilazonosítók nélküli kézbesítését, ami az alternatíváknál jobb metaadat-védelmet biztosít.", "privacy-matters-1-overlay-1-title": "Az adatvédelemmel pénzt spórol meg", @@ -99,26 +99,26 @@ "simplex-network-overlay-card-1-li-1": "A P2P-hálózatok az üzenetek továbbítására a DHT valamelyik változatát használják. A DHT kialakításakor egyensúlyt kell teremteni a kézbesítési garancia és a késleltetés között. A SimpleX jobb kézbesítési garanciával és alacsonyabb késleltetéssel rendelkezik, mint a P2P, mivel az üzenet redundánsan, a címzett által kiválasztott kiszolgálók segítségével több kiszolgálón keresztül párhuzamosan továbbítható. A P2P-hálózatokban az üzenet O(log N) csomóponton halad át szekvenciálisan, az algoritmus által kiválasztott csomópontok segítségével.", "simplex-network-overlay-card-1-li-2": "A SimpleX kialakítása a legtöbb P2P-hálózattól eltérően nem rendelkezik semmiféle globális felhasználói azonosítóval, még ideiglenessel sem, és csak az üzenetekhez használ ideiglenes, páros azonosítókat, ami jobb névtelenséget és metaadat-védelmet biztosít.", "simplex-network-overlay-card-1-li-3": "A P2P nem oldja meg a MITM-támadás problémát, és a legtöbb létező implementáció nem használ sávon kívüli üzeneteket a kezdeti kulcscseréhez. A SimpleX a kezdeti kulcscseréhez sávon kívüli üzeneteket, vagy bizonyos esetekben már meglévő biztonságos és megbízható kapcsolatokat használ.", - "simplex-network-overlay-card-1-li-6": "A P2P-hálózatok sebezhetőek lehetnek a DRDoS-támadással szemben, amikor a kliensek képesek a forgalmat újraközvetíteni és felerősíteni, ami az egész hálózatra kiterjedő szolgáltatásmegtagadást eredményez. A SimpleX kliensek csak az ismert kapcsolatból származó forgalmat továbbítják, és a támadó nem használhatja őket arra, hogy az egész hálózatban felerősítse a forgalmat.", + "simplex-network-overlay-card-1-li-6": "A P2P-hálózatok sebezhetőek lehetnek a DRDoS-támadással szemben, amikor a kliensek képesek a forgalmat újraközvetíteni és felerősíteni, ami az egész hálózatra kiterjedő szolgáltatásmegtagadást eredményez. A SimpleX kliensek csak az ismert kapcsolatokból származó forgalmat továbbítják, és a támadó nem használhatja őket arra, hogy az egész hálózatban felerősítse a forgalmat.", "simplex-network-overlay-card-1-li-5": "Minden ismert P2P-hálózat sebezhető Sybil támadással, mert minden egyes csomópont felderíthető, és a hálózat egészként működik. A támadások enyhítésére szolgáló ismert intézkedés lehet egy központi kiszolgáló (például: tracker), vagy egy drága tanúsítvány. A SimpleX hálózat nem ismeri fel a kiszolgálókat, töredezett és több elszigetelt alhálózatként működik, ami lehetetlenné teszi az egész hálózatra kiterjedő támadásokat.", "privacy-matters-overlay-card-1-p-1": "Sok nagyvállalat arra használja fel a felhasználóival kapcsolatban álló személyek adatait, hogy megbecsülje a jövedelmi helyzetüket és olyan termékeket kínáljon fel, amelyekre valójában nincs is szükségük, valamint hogy meghatározza az árakat.", "privacy-matters-overlay-card-1-p-2": "Az online kiskereskedők tudják, hogy az alacsonyabb jövedelműek nagyobb valószínűséggel vásárolnak azonnal, ezért magasabb árakat számíthatnak fel, vagy eltörölhetik a kedvezményeket.", "privacy-matters-overlay-card-1-p-3": "Egyes pénzügyi és biztosítótársaságok szociális grafikonokat használnak a kamatlábak és a díjak meghatározásához. Ez gyakran arra készteti az alacsonyabb jövedelmű embereket, hogy többet fizessenek — ez az úgynevezett „szegénységi prémium”.", "privacy-matters-overlay-card-1-p-4": "A SimpleX hálózat minden alternatívánál jobban védi a kapcsolatai adatait, teljes mértékben megakadályozva, hogy az ismeretségi-hálója bármilyen vállalat vagy szervezet számára elérhetővé váljon. Még ha az emberek a SimpleX Chat által előre beállított kiszolgálókat is használják, sem az alkalmazások, sem a kiszolgálók üzemeltetői nem ismerik, sem felhasználók számát, sem a kapcsolataikat.", "privacy-matters-overlay-card-2-p-1": "Nem is olyan régen megfigyelhettük, hogy a nagy választásokat manipulálta egy neves tanácsadó cég, amely az ismeretségi-háló segítségével eltorzította a valós világról alkotott képünket, és manipulálta a szavazatainkat.", - "privacy-matters-overlay-card-2-p-2": "Ahhoz, hogy objektív legyen és független döntéseket tudjon hozni, az információs terét is kézben kell tartania. Ez csak akkor lehetséges, ha privát kommunikációs hálózatot használ, amely nem fér hozzá az ismeretségi hálójához.", + "privacy-matters-overlay-card-2-p-2": "Ahhoz, hogy objektív legyen és független döntéseket tudjon hozni, az információs terét is kézben kell tartania. Ez csak akkor lehetséges, ha egy privát kommunikációs hálózatot használ, amely nem fér hozzá az ismeretségi hálójához.", "privacy-matters-overlay-card-2-p-3": "A SimpleX az első olyan hálózat, amely eleve nem rendelkezik felhasználói azonosítókkal, így jobban védi az ismeretségi-hálóját, mint bármely ismert alternatíva.", "privacy-matters-overlay-card-3-p-1": "Mindenkinek törődnie kell a magánélet és a kommunikáció biztonságával — az ártalmatlan beszélgetések veszélybe sodorhatják, még akkor is, ha nincs semmi rejtegetnivalója.", "privacy-matters-overlay-card-3-p-2": "Az egyik legmegdöbbentőbb a Mohamedou Ould Salahi memoárjában leírt és az „A mauritániai” c. filmben bemutatott történet. Őt bírósági tárgyalás nélkül a guantánamói táborba zárták, és ott kínozták 15 éven át, miután egy afganisztáni rokonát telefonon felhívta, akit azzal gyanúsítottak a hatóságok, hogy köze van a 9/11-es merényletekhez, holott Salahi az előző 10 évben Németországban élt.", "privacy-matters-overlay-card-3-p-3": "Átlagos embereket letartóztatnak azért, amit online megosztanak, még „névtelen” fiókjaikon keresztül is, még demokratikus országokban is.", - "privacy-matters-overlay-card-3-p-4": "Nem elég csak egy végpontok között titkosított üzenetváltó-alkalmazást használnunk, mindannyiunknak olyan üzenetváltó-alkalmazásokat kell használnunk, amelyek védik a személyes partnereink magánéletét — akikkel kapcsolatban állunk.", + "privacy-matters-overlay-card-3-p-4": "Nem elég csak egy végpontok között titkosított üzenetváltó alkalmazást használnunk, mindannyiunknak olyan üzenetváltó alkalmazásokat kell használnunk, amelyek védik a személyes partnereink magánéletét — akikkel kapcsolatban állunk.", "simplex-unique-overlay-card-1-p-1": "Más üzenetküldő hálózatoktól eltérően a SimpleX nem rendel azonosítókat a felhasználókhoz. Nem támaszkodik telefonszámokra, tartomány-alapú címekre (mint az e-mail, XMPP vagy a Matrix), felhasználónevekre, nyilvános kulcsokra vagy akár véletlenszerű számokra a felhasználók azonosításához — a SimpleX kiszolgálók üzemeltetői nem tudják, hogy hányan használják a kiszolgálóikat.", "simplex-unique-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez a SimpleX egyirányú várólistákat használ az üzenetekhez, páronkénti, névtelen címekkel, külön a fogadott és külön az elküldött üzenetekhez, általában különböző kiszolgálókon keresztül.", "simplex-unique-overlay-card-1-p-3": "Ez a kialakítás védi partnerei adatait, elrejtve azt a SimpleX hálózat kiszolgálói és a külső megfigyelők elől. Az IP-címe elrejtésének érdekében a Tor hálózaton keresztül is kapcsolódhat a SimpleX kiszolgálókhoz.", "simplex-unique-overlay-card-2-p-1": "Mivel senki sem rendelkezik azonosítóval a SimpleX hálózaton, ezért senki sem tud kapcsolatba lépni Önnel, hacsak nem oszt meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", "simplex-unique-overlay-card-2-p-2": "Még a felhasználói cím használata esetén is, aminek használata nem kötelező – ugyanakkor ez a kéretlen kapcsolatkérelmek küldésére is használható – módosíthatja vagy teljesen törölheti a címet anélkül, hogy elveszítené a meglévő kapcsolatait.", "simplex-unique-overlay-card-3-p-1": "A SimpleX Chat az összes felhasználói adatot kizárólag a klienseken tárolja egy hordozható titkosított adatbázis-formátumban, amely exportálható és átvihető bármely más támogatott eszközre.", - "simplex-unique-overlay-card-3-p-2": "A végpontok között titkosított üzenetek átmenetileg a SimpleX továbbítókiszolgálóin tárolódnak, amíg meg nem érkeznek a címzetthez, majd automatikusan véglegesen törlődnek onnan.", + "simplex-unique-overlay-card-3-p-2": "A végpontok között titkosított üzenetek átmenetileg a SimpleX átjátszóin tárolódnak, amíg meg nem érkeznek a címzetthez, majd automatikusan véglegesen törlődnek onnan.", "simplex-unique-overlay-card-3-p-3": "A föderált hálózatok kiszolgálóitól (e-mail, XMPP vagy Matrix) eltérően a SimpleX kiszolgálók nem tárolják a felhasználói fiókokat, csak továbbítják az üzeneteket, így védve mindkét fél magánéletét.", "simplex-unique-overlay-card-3-p-4": "A küldött és a fogadott kiszolgálóforgalom között nincsenek közös azonosítók vagy titkosított szövegek — ha bárki megfigyeli, nem tudja könnyen megállapítani, hogy ki kivel kommunikál, még akkor sem, ha a TLS-t kompromittálják.", "simplex-unique-overlay-card-4-p-1": "Használhatja a SimpleXet a saját kiszolgálóival, és továbbra is kommunikálhat azokkal, akik az előre beállított kiszolgálókat használják az alkalmazásban.", @@ -128,7 +128,7 @@ "simplex-unique-card-1-p-2": "Minden más létező üzenetküldő hálózattól eltérően a SimpleX nem rendelkezik a felhasználókhoz rendelt azonosítókkal — még véletlenszerű számokkal sem.", "simplex-unique-card-2-p-1": "Mivel a SimpleX hálózaton senkinek sincs azonosítója vagy állandó címe, ezért senki sem tud kapcsolatba lépni a felhasználókkal, hacsak nem osztanak meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", "simplex-unique-card-3-p-1": "A SimpleX Chat az összes felhasználói adatot kizárólag a klienseken tárolja egy hordozható titkosított adatbázis-formátumban —, amely exportálható és átvihető bármely más támogatott eszközre.", - "simplex-unique-card-3-p-2": "A végpontok között titkosított üzenetek átmenetileg a SimpleX továbbítókiszolgálóin tartózkodnak, amíg be nem érkeznek a címzetthez, majd automatikusan véglegesen törlődnek onnan.", + "simplex-unique-card-3-p-2": "A végpontok között titkosított üzenetek átmenetileg a SimpleX átjátszóin tartózkodnak, amíg be nem érkeznek a címzetthez, majd automatikusan véglegesen törlődnek onnan.", "simplex-unique-card-4-p-1": "A SimpleX hálózat teljesen decentralizált és független bármely kriptopénztől vagy bármely más hálózattól, kivéve az internetet.", "simplex-unique-card-4-p-2": "Használhatja a SimpleXet a saját kiszolgálóival vagy az általunk biztosított kiszolgálókkal, és továbbra is kapcsolódhat bármely felhasználóhoz.", "join": "Csatlakozzon a közösségeinkhez", @@ -143,17 +143,17 @@ "learn-more": "Tudjon meg többet", "more-info": "További információ", "hide-info": "Információ elrejtése", - "contact-hero-subheader": "Olvassa be a QR-kódot a SimpleX Chat alkalmazással a telefonjával vagy táblagépével.", - "contact-hero-p-1": "A hivatkozásban szereplő nyilvános kulcsokat és az üzenetek várólistájának címe NEM kerül elküldésre a hálózaton keresztül, amikor megtekinti ezt az oldalt — azokat, a hivatkozás webcímének kivonattöredéke tartalmazza.", + "contact-hero-subheader": "Olvassa be a QR-kódot a SimpleX Chat alkalmazással az eszköze segítségével.", + "contact-hero-p-1": "A hivatkozásban szereplő nyilvános kulcsok és az üzenetek várólistájának címe NEM lesz elküldve a hálózaton keresztül, amikor megtekinti ezt az oldalt — azokat, a hivatkozás webcímének kivonattöredéke tartalmazza.", "contact-hero-p-2": "Még nem töltötte le a SimpleX Chatet?", - "contact-hero-p-3": "Az alkalmazás letöltéséhez használja az alábbi hivatkozásokat.", + "contact-hero-p-3": "Az alkalmazás letöltéséhez használja az alábbi hivatkozások egyikét.", "scan-qr-code-from-mobile-app": "QR-kód beolvasása mobilalkalmazásból", "to-make-a-connection": "A kapcsolat létrehozásához:", "install-simplex-app": "Telepítse a SimpleX alkalmazást", - "open-simplex-app": "Simplex alkalmazás megnyitása", + "open-simplex-app": "SimpleX alkalmazás megnyitása", "tap-the-connect-button-in-the-app": "Koppintson a „kapcsolódás” gombra az alkalmazásban", "scan-the-qr-code-with-the-simplex-chat-app": "Olvassa be a QR-kódot a SimpleX Chat alkalmazással", - "scan-the-qr-code-with-the-simplex-chat-app-description": "A hivatkozásban szereplő nyilvános kulcsokat és az üzenetek várólistájának címe NEM kerül elküldésre a hálózaton keresztül, amikor ezt az oldalt megtekinti —
    ezek a hivatkozás webcímének kivonattöredékében szerepelnek.", + "scan-the-qr-code-with-the-simplex-chat-app-description": "A hivatkozásban szereplő nyilvános kulcsok és az üzenetek várólistájának címe NEM lesz elküldve a hálózaton keresztül, amikor megtekinti ezt az oldalt —
    azokat, a hivatkozás webcímének kivonattöredéke tartalmazza.", "installing-simplex-chat-to-terminal": "A SimpleX chat telepítése a terminálhoz", "use-this-command": "Használja ezt a parancsot:", "see-simplex-chat": "Az utasításokat megtekintheti a SimpleX Chat", @@ -165,13 +165,13 @@ "copy-the-command-below-text": "másolja be az alábbi parancsot, és használja a csevegésben:", "privacy-matters-section-header": "Miért számít az adatvédelem", "privacy-matters-section-subheader": "A metaadatok — például, hogy kivel beszélget — védelmének megőrzése biztonságot nyújt a következők ellen:", - "privacy-matters-section-label": "Győződjön meg arról, hogy az üzenetváltó-alkalmazás amit használ nem fér hozzá az adataihoz!", + "privacy-matters-section-label": "Győződjön meg arról, hogy az üzenetváltó alkalmazás amit használ nem fér hozzá az adataihoz!", "simplex-private-section-header": "Mitől lesz a SimpleX privát", "simplex-network-section-header": "SimpleX hálózat", "simplex-network-section-desc": "A SimpleX Chat a P2P- és a föderált hálózatok előnyeinek kombinálásával biztosítja a legjobb adatvédelmet.", "simplex-network-1-desc": "Minden üzenet a kiszolgálókon keresztül kerül elküldésre, ami jobb metaadat-védelmet és megbízható aszinkron üzenetkézbesítést biztosít, miközben elkerülhető a sok", "simplex-network-2-header": "A föderált hálózatokkal ellentétben", - "simplex-network-2-desc": "A SimpleX továbbítókiszolgálói NEM tárolnak felhasználói profilokat, kapcsolatokat és kézbesített üzeneteket, NEM kapcsolódnak egymáshoz, és NINCS kiszolgálójegyzék.", + "simplex-network-2-desc": "A SimpleX átjátszó kiszolgálói NEM tárolnak felhasználói profilokat, kapcsolatokat és kézbesített üzeneteket, NEM kapcsolódnak egymáshoz, és NINCS kiszolgálójegyzék.", "simplex-network-3-header": "SimpleX hálózat", "simplex-network-3-desc": "a kiszolgálók egyirányú várólistákat biztosítanak a felhasználók összekapcsolásához, de nem látják a hálózati kapcsolati gráfot; azt csak a felhasználók látják.", "comparison-section-header": "Összehasonlítás más protokollokkal", @@ -191,13 +191,13 @@ "comparison-section-list-point-1": "Általában telefonszám alapján, néhány esetben felhasználónév alapján", "comparison-section-list-point-2": "DNS-alapú címek", "comparison-section-list-point-3": "Nyilvános kulcs vagy más globális egyedi azonosító", - "comparison-section-list-point-4a": "A SimpleX továbbítókiszolgálói nem veszélyeztethetik a végpontok közötti titkosítást. Hitelesítse a biztonsági kódot a sávon kívüli csatorna elleni támadások veszélyeinek csökkentésére", - "comparison-section-list-point-4": "Ha az üzemeltetett kiszolgálók veszélybe kerülnek. Hitelesítse a biztonsági kódot a Signal vagy más biztonságos üzenetküldő alkalmazás segítségével a támadások veszélyeinek csökkentésére", + "comparison-section-list-point-4a": "A SimpleX átjátszói nem veszélyeztethetik a végpontok közötti titkosítást. Ellenőrizze a biztonsági kódot a sávon kívüli csatorna elleni támadások veszélyeinek csökkentésére", + "comparison-section-list-point-4": "Ha az üzemeltetett kiszolgálók veszélybe kerülnek. Ellenőrizze a biztonsági kódot a Signal vagy más biztonságos üzenetküldő alkalmazás segítségével a támadások veszélyeinek csökkentésére", "comparison-section-list-point-5": "Nem védi a felhasználók metaadatait", "comparison-section-list-point-6": "Bár a P2P elosztott, de nem föderált — egyetlen hálózatként működnek", "comparison-section-list-point-7": "A P2P-hálózatoknak vagy van egy központi hitelesítője, vagy az egész hálózat kompromittálódhat", "see-here": "tekintse meg itt", - "guide-dropdown-1": "Gyors indítás", + "guide-dropdown-1": "Gyorsindítás", "guide-dropdown-2": "Üzenetek küldése", "guide-dropdown-3": "Titkos csoportok", "guide-dropdown-4": "Csevegési profilok", @@ -209,7 +209,7 @@ "docs-dropdown-1": "SimpleX hálózat", "docs-dropdown-2": "Android fájlok elérése", "docs-dropdown-3": "Hozzáférés a csevegési adatbázishoz", - "docs-dropdown-8": "SimpleX csoportjegyzék", + "docs-dropdown-8": "SimpleX-csoportjegyzék", "docs-dropdown-9": "Letöltések", "f-droid-page-simplex-chat-repo-section-text": "Ha hozzá szeretné adni az F-Droid klienséhez, olvassa be a QR-kódot, vagy használja ezt a webcímet:", "signing-key-fingerprint": "Az aláírókulcs ujjlenyomata (SHA-256)", @@ -219,8 +219,8 @@ "f-droid-page-f-droid-org-repo-section-text": "A SimpleX Chat és az F-Droid-tároló különböző kulcsokkal írják alá az összeállításokat. A váltáshoz exportálja a csevegési adatbázist és telepítse újra az alkalmazást.", "jobs": "Csatlakozzon a csapathoz", "please-enable-javascript": "Engedélyezze a JavaScriptet a QR-kód megjelenítéséhez.", - "please-use-link-in-mobile-app": "Használja a mobilalkalmazásban található hivatkozást", - "contact-hero-header": "Kapott egy meghívót a SimpleX Chaten való beszélgetéshez", + "please-use-link-in-mobile-app": "Használja a hivatkozást a SimpleX Chat alkalmazásban", + "contact-hero-header": "Ez egy meghívó a SimpleX Chaten való beszélgetéshez", "invitation-hero-header": "Kapott egy egyszer használható meghívót a SimpleX Chaten való beszélgetéshez", "simplex-network-overlay-card-1-li-4": "A P2P-megvalósításokat egyes internetszolgáltatók blokkolhatják (mint például a BitTorrent). A SimpleX átvitel-független — a szabványos webes protokollokon, például WebSocketsen keresztül is működik.", "simplex-private-card-4-point-2": "A SimpleX, Tor hálózaton keresztüli használatához telepítse az Orbot alkalmazást és engedélyezze a SOCKS5 proxyt (vagy a VPN-t az iOS-ban).", @@ -259,45 +259,46 @@ "directory": "Csoportjegyzék", "about-and-contact-us": "Névjegy és kapcsolat", "index-hero-h1": "
    Legyen
    szabad
    ", - "index-hero-p1": "Privát és biztonságos üzenetküldés.
    Az első olyan hálózat, ahol Ön a tulajdonosa saját partnereinek és csoportjainak.", + "index-hero-p1": "Az első hálózat felhasználói azonosítók nélkül.
    Az Ön névjegyei, csoportjai és csatornái az Öné.", "index-hero-download-desktop-btn-title": "SimpleX számítógépes alkalmazásának letöltése", "index-security-assessment-title": "Biztonsági auditok", "index-security-review-2022-title": "Biztonsági audit 2022", "index-security-review-2024-title": "Biztonsági audit 2024", "index-security-audits-label": "Biztonsági
    auditok", "index-publications-heise-title": "A Heise Online kiadványai", - "index-hero-h2": "Az Ön hálózatában", + "index-hero-h2": "A saját hálózatában", "index-testflight-title": "Nyilvános betekintés az iOS alkalmazás fejlesztésébe a TestFlighton", "index-f-droid-title": "SimpleX alkalmazás az F-Droidon keresztül", "index-publications-privacy-guides-title": "A Privacy Guides üzenetváltó ajánlásai", "index-publications-whonix-title": "Whonix ajánlás", "index-publications-kuketz-title": "Áttekintette: Mike Kuketz", "index-publications-optout-title": "OptOut podcast interjú", - "worlds-most-secure-messaging": "A világ legbiztonságosabb üzenetváltó alkalmazása", - "index-messaging-p1": "Az üzenetváltás a SimpleXben élvonalbeli végpontok közötti titkosítással rendelkezik.", - "index-messaging-p2": "A biztonsága és magánszférájának védelme érdekében a kiszolgálók nem látják az üzeneteit, és azt sem, hogy kivel beszélget.", + "worlds-most-secure-messaging": "Senki sem láthatja, kivel beszélget", + "index-messaging-p1": "Még a kiszolgálók sem – az összes üzenet véletlenszerű zajnak tűnik.", + "index-messaging-p2": "Naponta több tízmillió üzenetet kézbesítünk bizalmasan.", "index-messaging-cta": "Tudjon meg többet a SimpleX üzenetváltó alkalmazásról", - "index-nextweb-h2": "Vegye birtokba
    A jövő hálózatát", - "index-nextweb-p1": "A SimpleX abból a meggyőződésből jött létre, hogy a profilok, a kapcsolatok és a közösségek a felhasználók tulajdonát kell, hogy képezzék.", - "index-nextweb-p2": "Egy decentralizált hálózat, amelyet senki sem birtokol, lehetővé teszi a kapcsolatok létrehozását és az ötleteket megosztását szabadon és biztonságosan a hálózaton.", - "index-token-h2": "Időtálló közösségek", - "index-token-p1": "A jövőben közösségi utalványokkal támogathatja a kedvenc csoportjait.", - "index-token-p2": "Az utalványokkal fizetni tudja a kiszolgálókat, hogy a közösségek szabadok és függetlenek maradhassanak.", - "index-token-cta": "Tudjon meg többet, és szerezzen ingyenes NFT-t az előzetes tesztelésért.", + "index-nextweb-h2": "A hálózat
    az Öné", + "index-nextweb-p1": "Minden névjegy és csoport az Ön eszközén van, nem egy kiszolgáló adatbázisában.", + "index-nextweb-p2": "Egyetlen szervezet sem irányítja a hálózatot – bárki üzemeltethet kiszolgálókat.", + "index-token-h2": "A felhasználói finanszírozzák", + "index-token-p1": "A függetlenség megőrzéséhez a nagy csatornák és közösségek fizetni fognak a kiszolgálóikért.", + "index-token-p2": "Ez fedezi az infrastruktúrát, a szoftverfejlesztést és a hálózat irányítását.", + "index-token-cta": "Tudjon meg többet a Community Credits-ről", "index-roadmap-h2": "A SimpleX ütemterve a szabad internethez", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Skálázódás nagy közösségekre", - "index-roadmap-2025-desc": "Központosított platformok elhagyása", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Fenntartható közösségek és kiszolgálók", - "index-roadmap-2026-desc": "Közösségi utalványok elindítása", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Növelje közösségeit", - "index-roadmap-2027-desc": "Eszközök a közösségei népszerűsítéséhez", + "index-roadmap-now": "Most", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Skálázódás nagy közösségekre", + "index-roadmap-1-desc": "Központosított platformok elhagyása", + "index-roadmap-2": "2027. jún.", + "index-roadmap-2-title": "Fenntartható közösségek és kiszolgálók", + "index-roadmap-2-desc": "Community Credits elindítása", + "index-roadmap-3": "2027. dec.", + "index-roadmap-3-title": "Közösségek növelése", + "index-roadmap-3-desc": "Eszközök biztosítása a közösségek népszerűsítéséhez", "index-directory-h2": "Csatlakozzon a SimpleX közösségekhez", - "index-directory-p1": "Már emberek százezrei bíznak a SimpleXen való üzenetváltásban.", - "index-directory-p2": "Találja meg a kedvenc közösségeit a SimpleX csoportjegyzékében, vagy hozza létre a saját csoportját!", - "index-directory-cta": "SimpleX csoportjegyzék megtekintése", + "index-directory-p1": "Több mint 2 millió ember töltötte le a SimpleX alkalmazásokat.", + "index-directory-p2": "Találja meg csatornáit és közösségeit a csoportjegyzékben, vagy hozza létre a sajátját!", + "index-directory-cta": "SimpleX-csoportjegyzék megtekintése", "index-directory-users-group-title": "SimpleX felhasználók csoportja", "how-secure-comparison-title": "A végpontok közötti titkosítás összehasonlítása más üzenetváltó alkalmazásokkal", "how-secure-message-padding": "Tartalomkitöltés", @@ -315,16 +316,16 @@ "navbar-token": "Token", "navbar-old-site": "Régi oldal", "docs-dropdown-15": "Összeállítások ellenőrzése és reprodukálása", - "why-p2": "Senki sem követi nyomon a beszélgetéseit. Senki sem készít térképet az Ön kapcsolati hálójáról. A magánélet nem csak egy funkció, hanem egy életmód.", - "why-p3": "Amikor online vagyunk minden platform egy darabot kér tőlünk – nevet, telefonszámot, baráti kapcsolatokat. Elfogadtuk, hogy a kommunikáció ára az, hogy mások megtudják, hogy kivel beszélünk. Minden generáció, az emberek és a technológia is eddig így működött – telefon, e-mail, üzenetküldő programok, közösségi média. Úgy tűnt, ez az egyetlen lehetséges mód.", + "why-p2": "Senki sem követte nyomon a beszélgetéseinket. Senki sem készített térképet arról, hogy merre jártunk. A magánéletünk nem csak egy funkció volt, hanem az életmódunk.", + "why-p3": "Aztán felléptünk az internetre, és minden platform kért belőlünk egy darabot — nevet, telefonszámot, baráti kapcsolatokat. Elfogadtuk, hogy a kommunikáció ára az, hogy mások megtudják, hogy kivel beszélünk. Minden generáció, az emberek és a technológia is eddig így működött — telefon, e-mail, üzenetküldő programok, közösségi média. Úgy tűnt, ez az egyetlen lehetséges mód.", "why-p4": "De van egy másik lehetőség is. Egy hálózat, amelyben nincsenek telefonszámok. Nincsenek felhasználónevek. Nincsenek fiókok. Nincsenek semmiféle felhasználói azonosítók. Egy hálózat, amely összeköti az embereket és titkosított üzeneteket továbbít, anélkül, hogy tudná, ki csatlakozik hozzá.", - "why-p5": "Nem egy jobb zár mások ajtaján. Nem egy kedvesebb házmester, aki tiszteletben tartja az Ön magánéletét, de mégis nyilvántartást vezet minden látogatójáról. Ön itt nem csak egy vendég. Ön itt otthon van. Egyetlen „kíváncsiskodó” sem tekinthet bele a beszélgetéseibe, Ön itt szuverén.", + "why-p5": "Nem egy jobb zár mások ajtaján. Nem egy kedvesebb házmester, aki tiszteletben tartja az Ön magánéletét, de mégis nyilvántartást vezet minden látogatójáról. Ön itt nem csak egy vendég. Ön itt otthon van. Nincs az a hatalom, amely beléphetne ide — Ön itt szuverén.", "why-p6": "A beszélgetései Önhöz tartoznak, ahogy az internet megjelenése előtt is mindig így volt. A hálózat nem egy hely, amelyet meglátogat. Ez egy olyan hely, amelyet Ön hoz létre saját magának. És senki sem veheti el Öntől, függetlenül attól, hogy privát vagy nyilvános.", - "why-p7": "A legrégebbi emberi szabadság – beszélgetni az emberekkel anélkül, hogy mások megfigyelnének – olyan infrastruktúrán alapul, amely nem tudja elárulni.", - "why-p8": "Mert elpusztítottuk azt az erőt, amellyel megtudhatnánk, hogy Ön kicsoda. Hogy az Ön ereje soha ne kerülhessen mások kezébe.", + "why-p7": "A legrégebbi emberi szabadság — beszélgetni az emberekkel, anélkül, hogy mások megfigyelnének — olyan infrastruktúrán alapul, amely nem tudja elárulni.", + "why-p8": "Mert felszámoltuk a lehetőségét is annak, hogy megtudjuk, Ön kicsoda. Így az önrendelkezése soha nem kerülhet idegen kezekbe.", "why-tagline": "Legyen szabad a saját hálózatában.", "why-footer-link": "Miért készítjük", - "why-p1": "Ön fiók nélkül született.", + "why-p1": "Fiók nélkül születtünk.", "file": "Fájl", "file-desc": "Fájlok biztonságos küldése végpontok közötti titkosítással – felhasználói fiókok és nyomon követés nélkül.", "file-noscript": "A fájlátvitelhez JavaScript szükséges.", @@ -335,8 +336,8 @@ "file-title": "SimpleX fájlátvitel", "file-drop-text": "Húzzon ide", "file-drop-hint": "vagy", - "file-choose": "válasszon ki egy fájlt", - "file-max-size": "Legfeljebb 100 MB – a SimpleX Chat alkalmazás viszont 1 GB méretű fájlokat is támogat", + "file-choose": "Válasszon ki egy fájlt", + "file-max-size": "Legfeljebb 100 MB – a SimpleX Chat alkalmazás viszont 1 GB méretű fájlokat is támogat", "file-encrypting": "Titkosítás…", "file-uploading": "Feltöltés…", "file-cancel": "Mégse", @@ -347,7 +348,7 @@ "file-expiry": "A fájlok általában 48 óráig érhetők el.", "file-sec-1": "A fájl a böngészőben lett titkosítva – az útválasztók soha nem „látják” a fájl tartalmát, nevét és méretét.", "file-sec-2": "A titkosítási kulcs a hivatkozás kivonattöredékében található – soha nem kerül elküldésre semmilyen kiszolgálóra.", - "file-sec-3": "A nagyobb biztonság érdekében használja a SimpleX Chat alkalmazást.", + "file-sec-3": "A nagyobb biztonság érdekében, használja a SimpleX Chat alkalmazást.", "file-retry": "Újra", "file-downloading": "Letöltés…", "file-decrypting": "Visszafejtés…", diff --git a/website/langs/id.json b/website/langs/id.json index 614f5937c8..c766929416 100644 --- a/website/langs/id.json +++ b/website/langs/id.json @@ -260,7 +260,7 @@ "get-simplex": "Dapatkan aplikasi desktop SimpleX", "index-hero-h1": "
    Bebaslah
    ", "index-hero-h2": "Di Jaringan Anda", - "index-hero-p1": "Pesan yang privat dan aman.
    Jaringan pertama tempat Anda memiliki kontak dan grup Anda.", + "index-hero-p1": "Jaringan pertama tanpa ID pengguna.
    Anda pemilik kontak, grup, dan kanal Anda.", "index-hero-download-desktop-btn-title": "Unduh Aplikasi Desktop SimpleX", "index-testflight-title": "Pratinjau iOS publik di TestFlight", "index-f-droid-title": "Repositori SimpleX F-Droid", @@ -273,30 +273,31 @@ "index-publications-heise-title": "publikasi", "index-publications-kuketz-title": "tinjauan", "index-publications-optout-title": "wawancara podcast", - "worlds-most-secure-messaging": "Perpesanan Paling Aman di Dunia", - "index-messaging-p1": "Perpesanan SimpleX memiliki enkripsi end-to-end yang canggih.", - "index-messaging-p2": "Demi keamanan dan privasi Anda, server tidak dapat melihat pesan Anda atau dengan siapa Anda bicara.", + "worlds-most-secure-messaging": "Tidak Ada yang Bisa Melihat dengan Siapa Anda Bicara", + "index-messaging-p1": "Bahkan server pun tidak bisa – semua pesan terlihat seperti derau acak.", + "index-messaging-p2": "Puluhan juta pesan dikirim secara privat setiap hari.", "index-messaging-cta": "Pelajari lebih lanjut tentang perpesanan SimpleX", - "index-nextweb-h2": "Anda Pemilik
    Web Berikutnya", - "index-nextweb-p1": "SimpleX didirikan atas keyakinan bahwa Anda harus memiliki identitas, kontak, dan komunitas Anda sendiri.", - "index-nextweb-p2": "Jaringan yang terbuka dan terdesentralisasi membuat Anda terhubung dengan orang lain dan berbagi ide: bebas dan aman.", - "index-token-h2": "Komunitas yang Bertahan Lama", - "index-token-p1": "Anda akan mendukung grup favorit Anda dengan voucher Komunitas di masa mendatang.", - "index-token-p2": "Voucher akan membayar server, agar komunitas Anda tetap bebas dan independen.", - "index-token-cta": "Pelajari lebih lanjut tentang Voucher Komunitas", + "index-nextweb-h2": "Anda Pemilik
    Jaringannya", + "index-nextweb-p1": "Setiap kontak dan grup ada di perangkat Anda, bukan di basis data server.", + "index-nextweb-p2": "Tidak ada satu entitas pun yang mengendalikan jaringan – siapa saja bisa menjalankan server.", + "index-token-h2": "Didanai oleh Penggunanya", + "index-token-p1": "Untuk tetap independen, kanal dan komunitas besar akan membayar server mereka.", + "index-token-p2": "Ini akan mencakup infrastruktur, pengembangan perangkat lunak, dan tata kelola jaringan.", + "index-token-cta": "Pelajari lebih lanjut tentang Kredit Komunitas", "index-roadmap-h2": "SimpleX Roadmap Menuju Internet Bebas", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Skala ke Komunitas Besar", - "index-roadmap-2025-desc": "Menghindari platform terpusat", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Komunitas & Server Berkelanjutan", - "index-roadmap-2026-desc": "Perilisan Voucher Komunitas", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Buat Komunitas Anda Meningkat", - "index-roadmap-2027-desc": "Alat untuk promosikan komunitas Anda", + "index-roadmap-now": "Sekarang", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Skala ke Komunitas Besar", + "index-roadmap-1-desc": "Menghindari platform terpusat", + "index-roadmap-2": "Jun 2027", + "index-roadmap-2-title": "Komunitas & Server Berkelanjutan", + "index-roadmap-2-desc": "Peluncuran Kredit Komunitas", + "index-roadmap-3": "Des 2027", + "index-roadmap-3-title": "Buat Komunitas Anda Meningkat", + "index-roadmap-3-desc": "Alat untuk promosikan komunitas Anda", "index-directory-h2": "Gabung ke Komunitas SimpleX", - "index-directory-p1": "Ratusan ribu orang sudah memercayai perpesanan SimpleX.", - "index-directory-p2": "Temukan komunitas Anda di direktori SimpleX dan buat komunitas Anda sendiri!", + "index-directory-p1": "Lebih dari 2 juta orang telah mengunduh aplikasi SimpleX.", + "index-directory-p2": "Temukan kanal dan komunitas Anda di direktori dan buat milik Anda sendiri!", "index-directory-cta": "Lihat Direktori SimpleX", "index-directory-users-group-title": "Grup pengguna SimpleX", "how-secure-comparison-title": "Seberapa amankah enkripsi end-to-end di berbagai aplikasi perpesanan?", diff --git a/website/langs/it.json b/website/langs/it.json index c7f6679a74..a3ced52c8e 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -62,8 +62,8 @@ "simplex-network-overlay-card-1-li-2": "Il design di SimpleX, a differenza della maggior parte delle reti P2P, non ha identificatori utente globali di alcun tipo, nemmeno temporanei, e usa solo identificatori temporanei a coppie, garantendo una maggiore protezione dell'anonimato e dei metadati.", "simplex-network-overlay-card-1-li-4": "Le implementazioni P2P possono essere bloccate da alcuni fornitori di internet (come BitTorrent). SimpleX è indipendente dal trasporto — può funzionare su protocolli web standard, es. WebSocket.", "hero-overlay-card-2-p-1": "Quando gli utenti hanno identità permanenti, anche se si tratta solo di un numero casuale, come un Session ID, c'è il rischio che il fornitore o un malintenzionato possano osservare come gli utenti sono connessi e quanti messaggi inviano.", - "simplex-network-overlay-card-1-li-6": "Le reti P2P possono essere vulnerabili all'attacco DRDoS, quando i client possono ritrasmettere e amplificare il traffico, con conseguente \"denial of service\" a livello di rete. I client SimpleX si limitano a inoltrare il traffico da una connessione nota e non possono essere usati da un aggressore per amplificare il traffico nell'intera rete.", - "privacy-matters-overlay-card-1-p-4": "La rete di SimpleX protegge la privacy delle tue connessioni meglio di qualsiasi alternativa, impedendo completamente che il tuo grafico sociale sia disponibile a qualsiasi azienda o organizzazione. Anche quando le persone usano i server preconfigurati in SimpleX Chat, gli operatori dei server non conoscono il numero di utenti o le loro connessioni.", + "simplex-network-overlay-card-1-li-6": "Le reti P2P possono essere vulnerabili all'attacco DRDoS, quando i client possono ritrasmettere e amplificare il traffico, con conseguente \"denial of service\" a livello di rete. I client SimpleX si limitano a inoltrare il traffico da connessioni note e non possono essere usati da un aggressore per amplificare il traffico nell'intera rete.", + "privacy-matters-overlay-card-1-p-4": "La rete di SimpleX protegge la privacy delle tue connessioni meglio di qualsiasi alternativa, impedendo completamente che il tuo grafico sociale diventi disponibile a qualsiasi azienda o organizzazione. Anche quando le persone usano i server preconfigurati in SimpleX Chat, gli operatori dei server non conoscono il numero di utenti o le loro connessioni.", "privacy-matters-overlay-card-2-p-3": "SimpleX è la prima rete che non ha alcun identificatore utente per design, proteggendo così il tuo grafico delle connessioni meglio di qualsiasi alternativa conosciuta.", "privacy-matters-overlay-card-3-p-1": "Tutti dovrebbero preoccuparsi della privacy e della sicurezza delle proprie comunicazioni — conversazioni innocue possono metterti in pericolo, anche se non hai nulla da nascondere.", "privacy-matters-overlay-card-3-p-3": "Le persone comuni vengono arrestate per ciò che condividono online, anche tramite i loro account \"anonimi\", anche nei Paesi democratici.", @@ -139,7 +139,7 @@ "simplex-private-card-6-point-1": "Molte reti di comunicazione sono vulnerabili agli attacchi MITM da parte di server o fornitori di rete.", "simplex-private-card-7-point-1": "Per garantire l'integrità, i messaggi sono numerati in sequenza e includono l'hash del messaggio precedente.", "simplex-private-card-9-point-2": "Riduce i vettori di attacco, rispetto ai broker di messaggi tradizionali, e i metadati disponibili.", - "simplex-private-card-10-point-2": "Ciò consente di recapitare messaggi senza identificatori del profilo utente, garantendo una migliore privacy dei metadati rispetto alle alternative.", + "simplex-private-card-10-point-2": "Ciò consente ai messaggi di venire recapitati senza identificatori del profilo utente, garantendo una migliore privacy dei metadati rispetto alle alternative.", "privacy-matters-1-title": "Pubblicità e discriminazione dei prezzi", "privacy-matters-2-overlay-1-title": "La privacy ti dà potere", "simplex-private-card-9-point-1": "Ogni coda di messaggi passa i messaggi in una direzione, con i diversi indirizzi di invio e ricezione.", @@ -181,7 +181,7 @@ "privacy-matters-overlay-card-1-p-2": "I rivenditori online sanno che le persone con redditi più bassi sono più propense a fare acquisti urgenti, quindi possono applicare prezzi più alti o rimuovere sconti.", "privacy-matters-overlay-card-1-p-3": "Alcune società finanziarie e assicurative usano grafici sociali per determinare i tassi di interesse e i premi. Spesso ciò fa pagare di più le persone con redditi più bassi — è noto come \"premio di povertà\".", "privacy-matters-overlay-card-2-p-1": "Non molto tempo fa abbiamo assistito alla manipolazione delle principali elezioni da una rispettabile società di consulenza che ha usato i nostri grafici sociali per distorcere la nostra visione del mondo reale e manipolare i nostri voti.", - "privacy-matters-overlay-card-2-p-2": "Per essere obiettivi e prendere decisioni indipendenti devi avere il controllo del tuo spazio informativo. È possibile solo se utilizzi una rete di comunicazione privata che non ha accesso al tuo grafico sociale.", + "privacy-matters-overlay-card-2-p-2": "Per essere obiettivi e prendere decisioni indipendenti devi avere il controllo del tuo spazio informativo. È possibile solo se usi una rete di comunicazione privata che non ha accesso al tuo grafico sociale.", "privacy-matters-overlay-card-3-p-2": "Una delle storie più scioccanti è l'esperienza di Mohamedou Ould Salahi descritta nel suo libro di memorie e mostrata nel film The Mauritanian. È stato rinchiuso nel campo di Guantánamo, senza processo, e lì è stato torturato per 15 anni dopo una telefonata a un suo parente in Afghanistan, sospettato di essere coinvolto negli attacchi dell'11/9, nonostante avesse vissuto in Germania per i precedenti 10 anni.", "join-us-on-GitHub": "Unisciti a noi su GitHub", "simplex-chat-for-the-terminal": "SimpleX Chat per il terminale", @@ -259,7 +259,7 @@ "about-and-contact-us": "Informazioni e contatti", "directory": "Directory", "index-hero-h2": "nella tua rete", - "index-hero-p1": "Messaggistica privata e sicura.
    La prima rete in cui possiedi
    i tuoi contatti e i tuoi gruppi.", + "index-hero-p1": "La prima rete senza ID utente.
    I tuoi contatti, gruppi e canali sono tuoi.", "index-hero-download-desktop-btn-title": "Scarica l'app desktop di SimpleX", "index-testflight-title": "Anteprima pubblica per iOS su TestFlight", "index-f-droid-title": "SimpleX via F-Droid", @@ -273,30 +273,31 @@ "index-publications-heise-title": "Pubblicazioni di Heise Online", "index-publications-kuketz-title": "Recensione di Mike Kuketz", "index-publications-optout-title": "Intervista podcast di OptOut", - "worlds-most-secure-messaging": "La messaggistica più sicura del mondo", - "index-messaging-p1": "SimpleX usa una crittografia end-to-end all'avanguardia.", - "index-messaging-p2": "Per la tua sicurezza e privacy, i server non possono vedere i messaggi e con chi parli.", + "worlds-most-secure-messaging": "Nessuno può vedere con chi parli", + "index-messaging-p1": "Nemmeno i server – tutti i messaggi appaiono come rumore casuale.", + "index-messaging-p2": "Decine di milioni di messaggi recapitati privatamente ogni giorno.", "index-messaging-cta": "Scopri di più sui messaggi di SimpleX", - "index-nextweb-h2": "Il nuovo web
    è tuo", - "index-nextweb-p1": "SimpleX è stato creato sulla convinzione che devi possedere i tuoi profili, contatti e comunità.", - "index-nextweb-p2": "Una rete decentralizzata che nessuno possiede consente di connetterti con persone e condividere idee, di restare libero e sicuro nella tua rete.", - "index-token-h2": "Comunità fatte per restare", - "index-token-p1": "Sosterrai i tuoi gruppi preferiti con futuri buoni comunitari.", - "index-token-p2": "I buoni pagheranno i server, per consentire alle tue comunità di rimanere libere e indipendenti.", - "index-token-cta": "Scopri di più e ricevi un pass di accesso gratuito per provarlo in anticipo.", + "index-nextweb-h2": "La rete
    è tua", + "index-nextweb-p1": "Ogni contatto e gruppo è sul tuo dispositivo, non nel database di un server.", + "index-nextweb-p2": "Nessuna singola entità controlla la rete – chiunque può gestire server.", + "index-token-h2": "Finanziato dai suoi utenti", + "index-token-p1": "Per restare indipendenti, i grandi canali e le comunità pagheranno per i propri server.", + "index-token-p2": "Questo coprirà infrastruttura, sviluppo software e governance della rete.", + "index-token-cta": "Scopri di più sui Crediti Comunitari", "index-roadmap-h2": "Tabella di marcia per un internet libero", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Scalabilità per comunità numerose", - "index-roadmap-2025-desc": "Fuga da piattaforme centralizzate", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Comunità e server sostenibili", - "index-roadmap-2026-desc": "Pubblicazione di buoni comunitari", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Fare crescere le tue comunità", - "index-roadmap-2027-desc": "Strumenti per promuovere le tue comunità", + "index-roadmap-now": "Ora", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Scalabilità per comunità numerose", + "index-roadmap-1-desc": "Fuga da piattaforme centralizzate", + "index-roadmap-2": "Giu 2027", + "index-roadmap-2-title": "Comunità e server sostenibili", + "index-roadmap-2-desc": "Lancio dei Crediti Comunitari", + "index-roadmap-3": "Dic 2027", + "index-roadmap-3-title": "Fare crescere le tue comunità", + "index-roadmap-3-desc": "Strumenti per promuovere le tue comunità", "index-directory-h2": "Unisciti alle comunità di SimpleX", - "index-directory-p1": "Centinaia di migliaia di persone si fidano già di SimpleX.", - "index-directory-p2": "Trova le comunità nella directory di SimpleX e creane una tua!", + "index-directory-p1": "Più di 2 milioni di persone hanno scaricato le app SimpleX.", + "index-directory-p2": "Trova i tuoi canali e le comunità nella directory e creane di tuoi!", "index-directory-cta": "Vedi la directory di SimpleX", "index-directory-users-group-title": "Gruppo utenti SimpleX", "how-secure-comparison-title": "Confronto della sicurezza di crittografia end-to-end in diverse app di messaggistica", @@ -315,14 +316,14 @@ "navbar-token": "Token", "navbar-old-site": "Sito vecchio", "docs-dropdown-15": "Verifica e riproduci le build", - "why-p2": "Nessuno monitorava le tue conversazioni. Nessuno disegnava una mappa delle tue posizioni. La privacy non era mai una caratteristica, era uno stile di vita.", - "why-p3": "Poi ci siamo trasferiti online e ogni piattaforma ha chiesto un pezzo di noi: il nome, il numero, gli amici. Abbiamo accettato che il prezzo da pagare per comunicare con gli altri fosse quello di far sapere a qualcuno con chi parliamo. Ogni generazione, sia le persone che la tecnologia, ha funzionato così: telefono, email, messenger, social media. Sembrava l'unica via possibile.", + "why-p2": "Nessuno monitorava le tue conversazioni. Nessuno disegnava una mappa delle tue posizioni. La privacy non era mai stata una caratteristica, era uno stile di vita.", + "why-p3": "Poi ci siamo trasferiti online e ogni piattaforma ha chiesto un pezzo di noi: il nome, il numero, gli amici. Abbiamo accettato che il prezzo da pagare per comunicare con gli altri fosse quello di far sapere a qualcuno con chi parliamo. Ogni generazione, sia di persone che di tecnologia, ha funzionato così: telefono, email, messenger, social media. Sembrava l'unico modo possibile.", "why-p1": "Sei nato senza un account.", - "why-p4": "C'è un altro modo. Una rete senza numeri di telefono. Senza nomi utente. Senza account. Senza identificatori utente di alcun tipo. Una rete che connette le persone e trasferisce messaggi crittografati senza sapere chi è connesso.", + "why-p4": "C'è un'altra via. Una rete senza numeri di telefono. Senza nomi utente. Senza account. Senza identificatori utente di alcun tipo. Una rete che connette le persone e trasferisce messaggi crittografati senza sapere chi è connesso.", "why-p5": "Non una serratura migliore sulla porta di qualcun altro. Non un padrone di casa più gentile che rispetta la tua privacy, ma che continua a tenere traccia di tutti i visitatori. Non sei un ospite. Sei a casa tua. Nessun re può entrarvi: sei tu il sovrano.", - "why-p6": "Le tue conversazioni appartengono a te, come è sempre stato prima dell'avvento di Internet. La rete non è un luogo che visiti. È un luogo che crei e possiedi. E nessuno può portartelo via, sia che tu lo renda privato o pubblico.", + "why-p6": "Le tue conversazioni appartengono a te, come è sempre stato prima dell'avvento di internet. La rete non è un luogo che visiti. È un luogo che crei e possiedi. E nessuno può portartelo via, che tu lo renda privato o pubblico.", "why-p7": "La più antica libertà umana, parlare con un'altra persona senza essere osservati, si basa su un'infrastruttura che non può tradirla.", - "why-p8": "Perché abbiamo distrutto il potere di sapere chi sei. In modo che il tuo potere non possa mai essere sottratto.", + "why-p8": "Perché abbiamo distrutto il potere di sapere chi sei. In modo che il tuo potere non possa mai esserti sottratto.", "why-tagline": "Vivi libero nella tua rete.", "why-footer-link": "Perché lo stiamo costruendo", "file": "File", @@ -336,7 +337,7 @@ "file-drop-text": "Trascina un file qui", "file-drop-hint": "o", "file-choose": "Scegli file", - "file-max-size": "Max 100 MB - L'app SimpleX Chat supporta file fino a 1 GB", + "file-max-size": "Max 100 MB - L'app SimpleX Chat supporta file fino a 1 GB", "file-encrypting": "Crittografia…", "file-uploading": "Caricamento…", "file-cancel": "Annulla", @@ -347,7 +348,7 @@ "file-expiry": "I file sono generalmente disponibili per 48 ore.", "file-sec-1": "Il file è stato crittografato nel browser: gli instradatori di dati non vedono mai il contenuto, il nome o la dimensione del file.", "file-sec-2": "La chiave di crittografia è nel frammento hash del link, non viene mai inviata ad alcun server.", - "file-sec-3": "Per una migliore sicurezza, usa l'app SimpleX Chat.", + "file-sec-3": "Per una migliore sicurezza, usa l'app SimpleX Chat.", "file-retry": "Riprova", "file-downloading": "Scaricamento…", "file-decrypting": "Decifrazione…", diff --git a/website/langs/ja.json b/website/langs/ja.json index 2337ed472d..8d8baee0c5 100644 --- a/website/langs/ja.json +++ b/website/langs/ja.json @@ -46,7 +46,7 @@ "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-2-p-2": "サーバーは、ユーザーの会話や接続の全体を把握することなく、メッセージを一方向に送信するだけです。", "simplex-explained-tab-3-p-1": "サーバーはキューごとに個別の匿名認証情報を持っており、どのユーザーに属しているかはわかりません。", "simplex-explained-tab-3-p-2": "ユーザーは、Tor を使用してサーバーにアクセスし、IP アドレスによる相関を防ぐことで、メタデータのプライバシーをさらに向上させることができます。", "chat-protocol": "チャットプロトコル", @@ -63,7 +63,7 @@ "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年間拷問を受けました。", + "privacy-matters-overlay-card-3-p-2": "最も衝撃的な話の 1 つは、Mohamedou Ould Salahiの経験であり、彼の回顧録に記述され、『モーリタニア映画』で紹介されています。彼はアフガニスタンの親戚に電話をかけた後、アメリカ同時多発テロへの関与の疑いで拘束されました。 彼は裁判も受けずにグアンタナモ収容所に入れられ、そこで15年間拷問を受けました。", "signing-key-fingerprint": "署名キーのフィンガープリント (SHA-256)", "simplex-network-2-desc": "SimpleX リレー サーバーは、ユーザー プロファイル、連絡先、配信されたメッセージを保存せず、相互に接続せず、サーバー ディレクトリもありません。", "docs-dropdown-5": "ホストXFTPサーバー", @@ -99,7 +99,7 @@ "privacy-matters-section-subheader": "メタデータのプライバシーを保護する — 話す相手 — 以下のことからあなたを守ります:", "if-you-already-installed": "すでにインストールしている場合", "join": "参加", - "privacy-matters-section-header": "プライバシーが重要である理由", + "privacy-matters-section-header": "なぜプライバシーが重要なのか", "on-this-page": "このページでは", "privacy-matters-overlay-card-1-p-2": "オンライン小売業者は、収入が低い人ほど急ぎの買い物をする可能性が高いことを知っているため、より高い価格を請求したり、割引を廃止したりすることがあります。", "simplex-unique-3-overlay-1-title": "データの所有権、管理、セキュリティ", @@ -180,7 +180,7 @@ "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 を使用します:", + "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": "選挙操作", @@ -189,15 +189,15 @@ "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": "一般の人が、たとえ「匿名」アカウント経由であっても、オンラインで共有した内容で逮捕されます。たとえ民主主義国家であったとしても。", + "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-unique": "なぜSimpleXなのか唯一", + "why-simplex-is-unique": "なぜSimpleXが唯一無二なのか", "simplex-network-section-header": "SimpleX ネットワーク", "simplex-private-10-title": "一時的な匿名のペア識別子", "privacy-matters-1-overlay-1-linkText": "プライバシーの保護はコストを削減します", - "tap-the-connect-button-in-the-app": "アプリの 「接続」 ボタンをタップします", + "tap-the-connect-button-in-the-app": "アプリの 「接続」 ボタンをタップしてください", "comparison-section-list-point-4a": "SimpleX リレーは e2e 暗号化を侵害できません。 セキュリティ コードを検証して帯域外チャネルへの攻撃を軽減します", "simplex-network-1-overlay-linktext": "P2Pネットワークの問題点", "no-private": "いいえ - プライベート", @@ -208,7 +208,7 @@ "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-1": "つい最近まで、主要な選挙が 有名なコンサルティング会社によって操作されていました。 ソーシャルグラフは私たちの現実世界の見方を歪め、投票を操作しています。", "privacy-matters-overlay-card-2-p-3": "SimpleX は、設計上ユーザー識別子を持たない最初のネットワークであり、この方法で既知の代替手段よりも接続グラフを保護します。", "learn-more": "さらに詳しく", "simplex-private-8-title": "メッセージのミキシング
    相関性を減らす", @@ -220,7 +220,7 @@ "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 ディレクトリ サービス", + "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 コードまたはリンクとして共有しない限り、誰もあなたに連絡することはできません。", @@ -233,9 +233,9 @@ "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-p-1": "P2P メッセージング プロトコルとアプリには、SimpleX よりも信頼性が低く、分析がより複雑になるさまざまな問題があり、また、いくつかの種類の攻撃に対して脆弱です。", "simplex-network-overlay-card-1-li-1": "P2P ネットワークは、メッセージをルーティングするために DHT の一部の変種に依存します。 DHT の設計では、配信保証と遅延のバランスを取る必要があります。 SimpleX は、受信者が選択したサーバーを使用して、メッセージを複数のサーバーを介して並行して冗長的に渡すことができるため、P2P よりも優れた配信保証と低い遅延の両方を備えています。 P2P ネットワークでは、メッセージはアルゴリズムによって選択されたノードを使用して、O(log N) 個のノードを順番に通過します。", - "privacy-matters-section-label": "メッセンジャーがあなたのデータにアクセスできないようにしてください!", + "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 のみが使用されます。", @@ -260,35 +260,36 @@ "about-and-contact-us": "概要・お問い合わせ", "index-hero-h1": "自由で
    あれ", "index-hero-h2": "あなたのネットワークで", - "index-hero-p1": "プライベートで安全なメッセージング。
    連絡先とグループをあなた自身が所有できる最初のネットワーク。", + "index-hero-p1": "ユーザーIDのない世界初のネットワーク。
    連絡先、グループ、チャンネルはあなたのものです。", "index-hero-download-desktop-btn-title": "SimpleX デスクトップアプリをダウンロード", "index-security-assessment-title": "セキュリティ監査", "index-security-review-2022-title": "セキュリティ監査 2022", "index-security-review-2024-title": "セキュリティ監査 2024", "index-security-audits-label": "セキュリティ
    監査", - "worlds-most-secure-messaging": "世界で最も安全なメッセージングサービス", - "index-messaging-p1": "SimpleXの通信は、最先端のエンドツーエンド暗号化によって保護されています。", - "index-messaging-p2": "安全とプライバシーのため、サーバーはメッセージの内容や、誰とやり取りしているかを知ることができません。", + "worlds-most-secure-messaging": "誰と話しているか、誰にも見えません", + "index-messaging-p1": "サーバーですら見ることはできません – すべてのメッセージはランダムなノイズに見えます。", + "index-messaging-p2": "毎日数千万件のメッセージがプライベートに配信されています。", "index-messaging-cta": "SimpleXのメッセージ機能について詳しく知る", - "index-nextweb-h2": "次のWebは
    あなたのもの", - "index-nextweb-p1": "SimpleXは、 アイデンティティ・連絡先・コミュニティはあなたのものであるべきだという考えに基づいています。", - "index-nextweb-p2": "オープンで分散型のネットワークで、自由で安全に人とつながり、アイデアを共有できます。", - "index-token-h2": "続いていくコミュニティ", - "index-token-p1": "コミュニティバウチャーを通じて、お気に入りのグループをサポートできます。", - "index-token-p2": "バウチャーはサーバー費用に充てられ、コミュニティが自由で独立した状態を保ち続けられるようにします。", + "index-nextweb-h2": "ネットワークは
    あなたのもの", + "index-nextweb-p1": "すべての連絡先とグループはあなたのデバイス上にあり、サーバーのデータベースにはありません。", + "index-nextweb-p2": "ネットワークを支配する単一の組織はありません – 誰でもサーバーを運用できます。", + "index-token-h2": "ユーザーの資金で運営", + "index-token-p1": "独立性を維持するため、大規模なチャンネルやコミュニティはサーバー費用を負担します。", + "index-token-p2": "これにより、インフラ、ソフトウェア開発、ネットワークガバナンスの費用が賄われます。", "index-roadmap-h2": "自由なインターネットを目指す SimpleX ロードマップ", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "大規模コミュニティへの拡張", - "index-roadmap-2025-desc": "中央集権型プラットフォームからの脱却", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "サステナブルなコミュニティ&サーバ", - "index-roadmap-2026-desc": "コミュニティバウチャーの開始", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "コミュニティの成長", - "index-roadmap-2027-desc": "コミュニティを広げるツール", + "index-roadmap-now": "現在", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "大規模コミュニティへの拡張", + "index-roadmap-1-desc": "中央集権型プラットフォームからの脱却", + "index-roadmap-2": "2027年6月", + "index-roadmap-2-title": "サステナブルなコミュニティ&サーバ", + "index-roadmap-2-desc": "コミュニティクレジットの開始", + "index-roadmap-3": "2027年12月", + "index-roadmap-3-title": "コミュニティの成長", + "index-roadmap-3-desc": "コミュニティを広げるツール", "index-directory-h2": "SimpleXコミュニティに参加する", - "index-directory-p1": "既に何十万人もの人々がSimpleXメッセージングを信頼しています。", - "index-directory-p2": "SimpleXディレクトリでコミュニティを見つけたり、あなた自身のコミュニティを作成しましょう!", + "index-directory-p1": "200万人以上がSimpleXアプリをダウンロードしました。", + "index-directory-p2": "ディレクトリでチャンネルやコミュニティを見つけて、あなた自身のものを作成しましょう!", "index-directory-cta": "SimpleXディレクトリを見る", "index-directory-users-group-title": "SimpleXユーザグループ", "index-publications-privacy-guides-title": "Privacy Guide 推奨", @@ -296,5 +297,10 @@ "index-publications-heise-title": "Heise Online の記事", "index-publications-kuketz-title": "Mike Kuketzによるレビュー", "index-publications-optout-title": "OptOut ポッドキャストインタビュー", - "send-file": "ファイルを送信" + "send-file": "ファイルを送信", + "navbar-old-site": "旧サイト", + "navbar-token": "トークン", + "docs-dropdown-15": "認証と再ビルド", + "index-f-droid-title": "F-Droid経由のSimpleXアプリ", + "how-secure-forward-secrecy": "前方秘匿性" } diff --git a/website/langs/pl.json b/website/langs/pl.json index 37582f80e3..9a9fe79d16 100644 --- a/website/langs/pl.json +++ b/website/langs/pl.json @@ -261,7 +261,7 @@ "about-and-contact-us": "O nas i Kontakt", "index-hero-h1": "Bądź
    Wolny", "index-hero-h2": "W Swojej Sieci", - "index-hero-p1": "Prywatne i bezpieczne wiadomości.
    Pierwsza sieć, w której posiadasz na własność swoje kontakty i grupy.", + "index-hero-p1": "Pierwsza sieć bez identyfikatorów użytkowników.
    Twoje kontakty, grupy i kanały należą do Ciebie.", "index-hero-download-desktop-btn-title": "Pobierz Aplikację SimpleX na Komputer", "index-testflight-title": "SimpleX iOS beta-wydanie na TestFlight", "index-f-droid-title": "SimpleX app z F-Droid", @@ -274,30 +274,31 @@ "index-publications-heise-title": "Publikacje Online Heise", "index-publications-kuketz-title": "Recenzja od Mike'a Kuketz'a", "index-publications-optout-title": "Wywiad w formie podcastu od OptOut", - "worlds-most-secure-messaging": "Najbardziej bezpieczne wiadomości na świecie", - "index-messaging-p1": "Komunikator SimpleX posiada najbardziej zaawansowane szyfrowanie end-to-end.", - "index-messaging-p2": "Dla Twojego bezpieczeństwa i prywatności, serwery nie mogą zobaczyć wiadomości i tego z kim rozmawiasz.", + "worlds-most-secure-messaging": "Nikt Nie Widzi Z Kim Rozmawiasz", + "index-messaging-p1": "Nawet serwery nie widzą – wszystkie wiadomości wyglądają jak losowy szum.", + "index-messaging-p2": "Dziesiątki milionów wiadomości dostarczanych prywatnie każdego dnia.", "index-messaging-cta": "Dowiedz się więcej o komunikatorze SimpleX", - "index-nextweb-h2": "Ty Posiadasz
    Sieć Kolejnej Generacji", - "index-nextweb-p1": "SimpleX powstał w oparciu o przekonanie, że musisz być właścicielem swoich profili, kontaktów i społeczności.", - "index-nextweb-p2": "Zdecentralizowana sieć, której nikt nie jest właścicielem, pozwala łączyć się z ludźmi i dzielić się pomysłami, zapewniając swobodę i bezpieczeństwo w sieci.", - "index-token-h2": "Społeczności, Które Trwają", - "index-token-p1": "Będziesz mógł wspierać swoje ulubione grupy dzięki przyszłym Voucherom Społeczności.", - "index-token-p2": "Vouchery opłacą serwery, aby Twoje społeczności pozostały wolne i niezależne.", - "index-token-cta": "Dowiedz się więcej i uzyskaj bezpłatną przepustkę umożliwiającą wczesne testowanie.", + "index-nextweb-h2": "Sieć Należy
    Do Ciebie", + "index-nextweb-p1": "Każdy kontakt i grupa znajduje się na Twoim urządzeniu, a nie w bazie danych serwera.", + "index-nextweb-p2": "Żaden podmiot nie kontroluje sieci – każdy może uruchomić serwer.", + "index-token-h2": "Finansowane Przez Użytkowników", + "index-token-p1": "Aby zachować niezależność, duże kanały i społeczności będą opłacać swoje serwery.", + "index-token-p2": "Pokryje to infrastrukturę, rozwój oprogramowania i zarządzanie siecią.", + "index-token-cta": "Dowiedz się więcej o Community Credits", "index-roadmap-h2": "Plan Działania SimpleX dla Wolnego Internetu", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Wyskalowany dla Dużych Społeczności", - "index-roadmap-2025-desc": "Wymyka się scentralizowanym platformom", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Zrównoważone Społeczności i Serwery", - "index-roadmap-2026-desc": "Uruchomienie Voucherów Społeczności", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Spraw, aby Twoje społeczności Rosły", - "index-roadmap-2027-desc": "Narzędzia do promowania Twoich społeczności", + "index-roadmap-now": "Teraz", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Wyskalowany dla Dużych Społeczności", + "index-roadmap-1-desc": "Wymyka się scentralizowanym platformom", + "index-roadmap-2": "Cze 2027", + "index-roadmap-2-title": "Zrównoważone Społeczności i Serwery", + "index-roadmap-2-desc": "Uruchomienie Community Credits", + "index-roadmap-3": "Gru 2027", + "index-roadmap-3-title": "Spraw, aby Twoje społeczności Rosły", + "index-roadmap-3-desc": "Narzędzia do promowania Twoich społeczności", "index-directory-h2": "Dołącz do Społeczności SimpleX", - "index-directory-p1": "Setki tysięcy ludzi już ufają wiadomościom SimpleX.", - "index-directory-p2": "Znajdź swoje społeczności w katalogu SimpleX i stwórz własne!", + "index-directory-p1": "Ponad 2 miliony osób pobrało aplikacje SimpleX.", + "index-directory-p2": "Znajdź swoje kanały i społeczności w katalogu i stwórz własne!", "index-directory-cta": "Zobacz katalog SimpleX", "index-directory-users-group-title": "Grupa użytkowników SimpleX", "how-secure-comparison-title": "Porównanie zabezpieczeń szyfrowania end-to-end w różnych komunikatorach", @@ -336,7 +337,7 @@ "file-drop-text": "Przeciągnij i upuść plik tutaj", "file-drop-hint": "lub", "file-choose": "Wybierz plik", - "file-max-size": "Maksymalnie 100 MB - aplikacja SimpleX Chat obsługuje pliki o rozmiarze do 1 GB", + "file-max-size": "Maksymalnie 100 MB - aplikacja SimpleX Chat obsługuje pliki o rozmiarze do 1 GB", "file-encrypting": "Szyfrowanie…", "file-uploading": "Wysyłanie…", "file-cancel": "Anuluj", diff --git a/website/langs/pt_BR.json b/website/langs/pt_BR.json index a94670e443..32b2ac5e05 100644 --- a/website/langs/pt_BR.json +++ b/website/langs/pt_BR.json @@ -261,7 +261,7 @@ "navbar-token": "Token", "index-hero-h1": "Seja
    Livre", "index-hero-h2": "Na Sua Rede", - "index-hero-p1": "Mensagens privadas e seguras.
    A primeira rede onde você é dono dos seus contatos e grupos.", + "index-hero-p1": "A primeira rede sem IDs de usuário.
    Seus contatos, grupos e canais pertencem a você.", "index-hero-download-desktop-btn-title": "Baixe o aplicativo SimpleX Desktop", "index-testflight-title": "SimpleX iOS - versão beta no TestFlight", "index-f-droid-title": "Aplicativo SimpleX no F-Droid", @@ -274,30 +274,31 @@ "index-publications-heise-title": "Publicações da Heise Online", "index-publications-kuketz-title": "Análise por Mike Kuketz", "index-publications-optout-title": "Entrevista no podcast OptOut", - "worlds-most-secure-messaging": "O sistema de mensagens mais seguro do mundo", - "index-messaging-p1": "O SimpleX possui criptografia de ponta a ponta de última geração.", - "index-messaging-p2": "Para sua segurança e privacidade, os servidores não podem ver suas mensagensnem com quem você conversa.", + "worlds-most-secure-messaging": "Ninguém Pode Ver Com Quem Você Conversa", + "index-messaging-p1": "Nem mesmo os servidores – todas as mensagens parecem ruído aleatório.", + "index-messaging-p2": "Dezenas de milhões de mensagens entregues de forma privada todos os dias.", "index-messaging-cta": "Saiba mais sobre SimpleX Messaging", - "index-nextweb-h2": "Você é Dono
    da Próxima Web", - "index-nextweb-p1": "SimpleX é fundado na crença de que você deve ser dono da sua identidade, contatos e comunidades.", - "index-nextweb-p2": "Rede aberta e descentralizada permite que você se conecte com pessoas e compartilhe ideias: seja livre e seguro.", - "index-token-h2": "Comunidades Duradouras", - "index-token-p1": "Você apoiará seus grupos favoritos com futuros Vouchers da Comunidade.", - "index-token-p2": "Os vouchers pagarão pelos servidores, permitindo que suas comunidades continuem gratuitas e independentes.", - "index-token-cta": "Saiba mais e pegue sua NFT gratuita
    para testes antecipados.", + "index-nextweb-h2": "A Rede
    É Sua", + "index-nextweb-p1": "Cada contato e grupo está no seu dispositivo, não no banco de dados de um servidor.", + "index-nextweb-p2": "Nenhuma entidade controla a rede – qualquer pessoa pode operar servidores.", + "index-token-h2": "Financiado Pelos Seus Usuários", + "index-token-p1": "Para manter a independência, grandes canais e comunidades pagarão pelos seus servidores.", + "index-token-p2": "Isso cobrirá infraestrutura, desenvolvimento de software e governança da rede.", + "index-token-cta": "Saiba mais sobre os Créditos da Comunidade", "index-roadmap-h2": "Roteiro do SimpleX para uma Internet Livre", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Escala para Grandes Comunidades", - "index-roadmap-2025-desc": "Fugindo de plataformas centralizadas", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Comunidades e Servidores Sustentáveis", - "index-roadmap-2026-desc": "Lançamento dos Vouchers da Comunidade", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Faça Suas Comunidades Crescerem", - "index-roadmap-2027-desc": "Ferramentas para promover suas comunidades", + "index-roadmap-now": "Agora", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Escala para Grandes Comunidades", + "index-roadmap-1-desc": "Fugindo de plataformas centralizadas", + "index-roadmap-2": "Jun 2027", + "index-roadmap-2-title": "Comunidades e Servidores Sustentáveis", + "index-roadmap-2-desc": "Lançamento dos Créditos da Comunidade", + "index-roadmap-3": "Dez 2027", + "index-roadmap-3-title": "Faça Suas Comunidades Crescerem", + "index-roadmap-3-desc": "Ferramentas para promover suas comunidades", "index-directory-h2": "Participe das Comunidades SimpleX", - "index-directory-p1": "Centenas de milhares de pessoas já confiam no SimpleX Messaging.", - "index-directory-p2": "Encontre suas comunidades no diretório SimpleX e crie a sua própria!", + "index-directory-p1": "Mais de 2 milhões de pessoas baixaram os aplicativos SimpleX.", + "index-directory-p2": "Encontre seus canais e comunidades no diretório e crie os seus próprios!", "index-directory-cta": "Ver diretório do SimpleX", "how-secure-comparison-title": "Comparação da segurança da criptografia de ponta a ponta em diferentes mensageiros", "how-secure-message-padding": "Preenchimento de mensagem", diff --git a/website/langs/ru.json b/website/langs/ru.json index 235492291f..ab968446af 100644 --- a/website/langs/ru.json +++ b/website/langs/ru.json @@ -260,7 +260,7 @@ "docs-dropdown-14": "SimpleX для бизнеса", "index-hero-h1": "
    Будь
    Свободен
    ", "index-hero-h2": "В Своей Сети", - "index-hero-p1": "Конфиденциальная и безопасная передача сообщений.
    Первая сеть, где Вам принадлежат Ваши контакты и группы.", + "index-hero-p1": "Первая сеть без идентификаторов пользователей.
    Ваши контакты, группы и каналы принадлежат Вам.", "index-hero-download-desktop-btn-title": "Загрузить приложение SimpleX для компьютера", "index-testflight-title": "Бета-релиз для iOS на TestFlight", "index-f-droid-title": "Загрузить через F-Droid", @@ -273,30 +273,31 @@ "index-publications-heise-title": "Публикации Heise Online", "index-publications-kuketz-title": "Обзор от Mike Kuketz", "index-publications-optout-title": "OptOut подкаст интервью", - "worlds-most-secure-messaging": "Самый Безопасный Мессенджер в Мире", - "index-messaging-p1": "Сообщения в SimpleX имеют самое передовое сквозное шифрование (end-to-end).", - "index-messaging-p2": "Для Вашей безопасности, серверы не могут видеть ваши сообщения и с кем Вы разговариваете.", + "worlds-most-secure-messaging": "Никто Не Знает С Кем Вы Общаетесь", + "index-messaging-p1": "Даже серверы – все сообщения выглядят как случайный шум.", + "index-messaging-p2": "Десятки миллионов сообщений доставляются конфиденциально каждый день.", "index-messaging-cta": "Узнать больше про сообщения в SimpleX", - "index-nextweb-h2": "Ваш Интернет Будущего", - "index-nextweb-p1": "SimpleX создан на убеждении, что Ваши профили, контакты и сообщества должны принадлежать Вам.", - "index-nextweb-p2": "Децентрализованная сеть, которой никто не владеет, позволяет Вам общаться и делиться идеями, оставаясь свободными и защищёнными в Вашей сети.", - "index-token-h2": "Стабильные Сообщества", - "index-token-p1": "Вы сможете поддерживать Ваши любимые группы с помощью будущих Ваучеров Групп.", - "index-token-p2": "Ваучеры будут использоваться для оплаты за серверы, чтобы группы оставались свободными и независимыми.", - "index-token-cta": "Узнайте больше и возьмите бесплатный пропуск, чтобы участвовать в тестировании.", + "index-nextweb-h2": "Сеть Принадлежит
    Вам", + "index-nextweb-p1": "Все контакты и группы хранятся на Вашем устройстве, а не в базе данных сервера.", + "index-nextweb-p2": "Ни одна организация не контролирует сеть – каждый может запускать серверы.", + "index-token-h2": "Финансируется Пользователями", + "index-token-p1": "Для сохранения независимости крупные каналы и сообщества будут оплачивать свои серверы.", + "index-token-p2": "Это покроет расходы на инфраструктуру, разработку программного обеспечения и управление сетью.", + "index-token-cta": "Узнать больше про Community Credits", "index-roadmap-h2": "Путь Сети SimpleX к Свободному Интернету", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Большие каналы и группы", - "index-roadmap-2025-desc": "Чтобы Вы могли покинуть централизованные платформы", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Самодостаточные группы и серверы", - "index-roadmap-2026-desc": "Запуск Ваучеров Групп", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Поддержка роста Ваших групп", - "index-roadmap-2027-desc": "Инструменты для продвижения групп", + "index-roadmap-now": "Сейчас", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Большие каналы и группы", + "index-roadmap-1-desc": "Чтобы Вы могли покинуть централизованные платформы", + "index-roadmap-2": "Июнь 2027", + "index-roadmap-2-title": "Самодостаточные группы и серверы", + "index-roadmap-2-desc": "Запуск Community Credits", + "index-roadmap-3": "Дек 2027", + "index-roadmap-3-title": "Поддержка роста Ваших групп", + "index-roadmap-3-desc": "Инструменты для продвижения групп", "index-directory-h2": "Вступайте в Группы SimpleX", - "index-directory-p1": "Сотни тысяч людей уже доверяют мессенджеру SimpleX.", - "index-directory-p2": "Найдите группы по душе в каталоге SimpleX и создайте свои!", + "index-directory-p1": "Более 2 миллионов человек скачали приложения SimpleX.", + "index-directory-p2": "Найдите каналы и сообщества в каталоге и создайте свои!", "index-directory-cta": "Открыть каталог SimpleX", "index-directory-users-group-title": "Группа пользователей SimpleX", "how-secure-comparison-title": "Сравнение безопасности сквозного шифрования в мессенджерах", diff --git a/website/langs/zh_Hans.json b/website/langs/zh_Hans.json index e67df60a6d..70f87ca79e 100644 --- a/website/langs/zh_Hans.json +++ b/website/langs/zh_Hans.json @@ -261,7 +261,7 @@ "docs-dropdown-15": "验证并重现构建过程", "index-hero-h1": "变得
    自由", "index-hero-h2": "在属于你的网络中", - "index-hero-p1": "私密安全的即时通讯。
    首个由您掌控
    联系人和群组的网络。", + "index-hero-p1": "首个无用户 ID 的网络。
    您的联系人、群组和频道由您掌控。", "index-hero-download-desktop-btn-title": "下载 SimpleX 桌面应用程序", "index-testflight-title": "SimpleX iOS 测试版已在 TestFlight 上发布", "index-f-droid-title": "SimpleX 安卓应用(通过 F-Droid)", @@ -274,27 +274,27 @@ "index-publications-heise-title": "Heise Online出版物", "index-publications-kuketz-title": "Mike Kuketz 的评论", "index-publications-optout-title": "OptOut播客访谈", - "worlds-most-secure-messaging": "全球最安全的即时通讯", - "index-messaging-p1": "SimpleX 即时通讯采用最先进的端到端加密技术。", - "index-messaging-p2": "为了您的安全和隐私,服务器无法看到您的消息以及您与谁交谈。", + "worlds-most-secure-messaging": "没人能看到您与谁交谈", + "index-messaging-p1": "即使服务器也无法查看 – 所有消息看起来都像随机噪声。", + "index-messaging-p2": "每天数千万条消息被私密送达。", "index-messaging-cta": "了解更多关于 SimpleX 消息传递的知识", - "index-nextweb-h2": "属于你的
    下一代互联网", - "index-nextweb-p1": "SimpleX 的创建理念是:您必须拥有自己的个人资料、联系人和社区。", - "index-nextweb-p2": "一个不归个人所有的去中心化网络,让你能够与他人联系并分享想法,在网络中自由安全地生活。", - "index-token-h2": "长久存在的社区", - "index-token-p1": "您将通过未来的社区代金券支持您喜爱的团体。", - "index-token-p2": "代金券将用于支付服务器费用,让您的社区保持自由和独立。", - "index-token-cta": "了解更多信息并获取免费抢先体验券,参与早期测试。", + "index-nextweb-h2": "网络
    由您掌控", + "index-nextweb-p1": "每个联系人和群组都在您的设备上,而非服务器数据库中。", + "index-nextweb-p2": "没有任何单一实体控制网络 – 任何人都可以运行服务器。", + "index-token-h2": "由用户资助", + "index-token-p1": "为保持独立性,大型频道和社区将为其服务器付费。", + "index-token-p2": "这将用于支付基础设施、软件开发和网络治理费用。", + "index-token-cta": "了解更多关于 Community Credits", "index-roadmap-h2": "SimpleX 通往自由互联网的路线图", - "index-roadmap-2025-title": "扩展到大型社区", - "index-roadmap-2025-desc": "逃离中心化平台", - "index-roadmap-2026-title": "可持续社区与服务器", - "index-roadmap-2026-desc": "推出社区代金券", - "index-roadmap-2027-title": "促进社区发展", - "index-roadmap-2027-desc": "用于推广社区的工具", + "index-roadmap-1-title": "扩展到大型社区", + "index-roadmap-1-desc": "逃离中心化平台", + "index-roadmap-2-title": "可持续社区与服务器", + "index-roadmap-2-desc": "推出 Community Credits", + "index-roadmap-3-title": "促进社区发展", + "index-roadmap-3-desc": "用于推广社区的工具", "index-directory-h2": "加入 SimpleX 社区", - "index-directory-p1": "已有数十万人信赖 SimpleX 即时通讯服务。", - "index-directory-p2": "在 SimpleX 目录中找到您的社区并创建您自己的社区!", + "index-directory-p1": "超过 200 万人下载了 SimpleX 应用。", + "index-directory-p2": "在目录中找到您的频道和社区,并创建您自己的!", "index-directory-cta": "查看 SimpleX 目录", "index-directory-users-group-title": "SimpleX 用户群组", "how-secure-comparison-title": "不同即时通讯软件端到端加密安全性的比较", @@ -332,7 +332,7 @@ "file-drop-text": "将文件拖放到此处", "file-drop-hint": "或", "file-choose": "选择文件", - "file-max-size": "最大支持 100 MB - SimpleX Chat 应用 支持最大 1 GB 的文件", + "file-max-size": "最大 100 MB - SimpleX Chat 应用 支持最大 1 GB 文件", "file-encrypting": "加密中……", "file-uploading": "正在上传…", "file-cancel": "取消", @@ -343,7 +343,7 @@ "file-expiry": "文件通常可保存 48 小时。", "file-sec-1": "您的文件已在浏览器中加密 - 数据路由器永远不会看到文件内容、名称或大小。", "file-sec-2": "加密密钥位于链接的哈希片段中——它永远不会发送到任何服务器。", - "file-sec-3": "为了获得更好的安全性,请使用SimpleX Chat应用程序。", + "file-sec-3": "为了获得更好的安全性,请使用SimpleX Chat应用程序。", "file-retry": "重试", "file-downloading": "正在下载…", "file-decrypting": "正在解密…", @@ -365,8 +365,9 @@ "file-proto-p-4": "文件被分割成多个片段后,会通过独立运营商运营的网络路由器进行传输。任何运营商都无法看到文件的实际大小或名称。即使路由器遭到入侵,也只能看到固定大小的加密片段。网络路由器会将文件片段缓存约48小时。", "file-proto-spec": "阅读 XFTP 协议规范 →", "navbar-token": "Token 令牌", - "index-roadmap-2025": "2025", - "index-roadmap-2026": "2026", - "index-roadmap-2027": "2027", + "index-roadmap-now": "现在", + "index-roadmap-1": "2026", + "index-roadmap-2": "2027年6月", + "index-roadmap-3": "2027年12月", "send-file": "发送文件" } diff --git a/website/package.json b/website/package.json index 9f4a5b12e7..6ad5deab6b 100644 --- a/website/package.json +++ b/website/package.json @@ -35,6 +35,7 @@ "gray-matter": "^4.0.3", "jsdom": "^22.1.0", "lottie-web": "5.12.2", - "markdown-it": "^13.0.1" + "markdown-it": "^13.0.1", + "markdown-it-footnote": "^4.0.0" } } diff --git a/website/src/_includes/blog_previews/20260430.html b/website/src/_includes/blog_previews/20260430.html new file mode 100644 index 0000000000..fad540ed2a --- /dev/null +++ b/website/src/_includes/blog_previews/20260430.html @@ -0,0 +1,3 @@ +

    Freedom of speech needs infrastructure that protects it by design — protocols, governance and funding.

    + +

    v6.5 release brings SimpleX Channels: a new model for online publishing built for participation privacy.

    diff --git a/website/src/_includes/navbar.html b/website/src/_includes/navbar.html index 9e1913879f..37daa78d3c 100644 --- a/website/src/_includes/navbar.html +++ b/website/src/_includes/navbar.html @@ -142,7 +142,7 @@ - {% if ('blog' not in page.url) and ('about' not in page.url) and ('donate' not in page.url) and ('privacy' not in page.url) and ('directory' not in page.url) and ('vouchers' not in page.url) and ('file' not in page.url) %} + {% if ('blog' not in page.url) and ('about' not in page.url) and ('donate' not in page.url) and ('privacy' not in page.url) and ('directory' not in page.url) and ('credits' not in page.url) and ('file' not in page.url) %}